偏远地区邮编判断调整
This commit is contained in:
@@ -27,6 +27,12 @@ public class PayBillController extends BaseController
|
|||||||
@Autowired
|
@Autowired
|
||||||
private IPayBillService payBillService;
|
private IPayBillService payBillService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询应付账单。
|
||||||
|
*
|
||||||
|
* @param query 账单号、供应商、状态、币种,空着的条件不过滤
|
||||||
|
* @return 账单列表,不含费用明细
|
||||||
|
*/
|
||||||
@PreAuthorize("@ss.hasPermi('price:payBill:list')")
|
@PreAuthorize("@ss.hasPermi('price:payBill:list')")
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
public TableDataInfo list(PayBill query)
|
public TableDataInfo list(PayBill query)
|
||||||
@@ -36,6 +42,12 @@ public class PayBillController extends BaseController
|
|||||||
return getDataTable(list);
|
return getDataTable(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询一张应付账单的明细和操作日志。
|
||||||
|
*
|
||||||
|
* @param billId 应付账单ID
|
||||||
|
* @return 账单头、费用行和日志
|
||||||
|
*/
|
||||||
@PreAuthorize("@ss.hasPermi('price:payBill:query')")
|
@PreAuthorize("@ss.hasPermi('price:payBill:query')")
|
||||||
@GetMapping("/{billId}")
|
@GetMapping("/{billId}")
|
||||||
public AjaxResult detail(@PathVariable Long billId)
|
public AjaxResult detail(@PathVariable Long billId)
|
||||||
@@ -43,6 +55,12 @@ public class PayBillController extends BaseController
|
|||||||
return success(payBillService.selectPayBillDetail(billId));
|
return success(payBillService.selectPayBillDetail(billId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预览本次能生成账单的订单。只查询,不重算费用,也不写账单。
|
||||||
|
*
|
||||||
|
* @param command 查询方式,以及订单号、计费日或提单号
|
||||||
|
* @return 可生成订单、未找到的单号、已经入账的订单
|
||||||
|
*/
|
||||||
@PreAuthorize("@ss.hasPermi('price:payBill:query')")
|
@PreAuthorize("@ss.hasPermi('price:payBill:query')")
|
||||||
@PostMapping("/preview")
|
@PostMapping("/preview")
|
||||||
public AjaxResult preview(@RequestBody PayBillCommand command)
|
public AjaxResult preview(@RequestBody PayBillCommand command)
|
||||||
@@ -50,6 +68,12 @@ public class PayBillController extends BaseController
|
|||||||
return success(payBillService.preview(command));
|
return success(payBillService.preview(command));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成应付账单。先重算每票订单的尾程应付,再把当时的费用抄进草稿账单。
|
||||||
|
*
|
||||||
|
* @param command 供应商,以及按订单号、计费日或提单号划定的范围
|
||||||
|
* @return 刚生成的草稿账单
|
||||||
|
*/
|
||||||
@PreAuthorize("@ss.hasPermi('price:payBill:add')")
|
@PreAuthorize("@ss.hasPermi('price:payBill:add')")
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public AjaxResult generate(@RequestBody PayBillCommand command)
|
public AjaxResult generate(@RequestBody PayBillCommand command)
|
||||||
@@ -57,6 +81,11 @@ public class PayBillController extends BaseController
|
|||||||
return success(payBillService.generate(command));
|
return success(payBillService.generate(command));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认草稿账单。确认后才可以登记付款。
|
||||||
|
*
|
||||||
|
* @param billId 应付账单ID
|
||||||
|
*/
|
||||||
@PreAuthorize("@ss.hasPermi('price:payBill:confirm')")
|
@PreAuthorize("@ss.hasPermi('price:payBill:confirm')")
|
||||||
@PutMapping("/confirm/{billId}")
|
@PutMapping("/confirm/{billId}")
|
||||||
public AjaxResult confirm(@PathVariable Long billId)
|
public AjaxResult confirm(@PathVariable Long billId)
|
||||||
@@ -65,6 +94,11 @@ public class PayBillController extends BaseController
|
|||||||
return success();
|
return success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把已确认的账单登记为已付款。已付款的账单不能直接作废。
|
||||||
|
*
|
||||||
|
* @param billId 应付账单ID
|
||||||
|
*/
|
||||||
@PreAuthorize("@ss.hasPermi('price:payBill:confirm')")
|
@PreAuthorize("@ss.hasPermi('price:payBill:confirm')")
|
||||||
@PutMapping("/paid/{billId}")
|
@PutMapping("/paid/{billId}")
|
||||||
public AjaxResult paid(@PathVariable Long billId)
|
public AjaxResult paid(@PathVariable Long billId)
|
||||||
@@ -73,6 +107,11 @@ public class PayBillController extends BaseController
|
|||||||
return success();
|
return success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作废应付账单。账单和费用行保留,只改状态并记下作废原因。
|
||||||
|
*
|
||||||
|
* @param command 账单ID和作废原因
|
||||||
|
*/
|
||||||
@PreAuthorize("@ss.hasPermi('price:payBill:void')")
|
@PreAuthorize("@ss.hasPermi('price:payBill:void')")
|
||||||
@PutMapping("/void")
|
@PutMapping("/void")
|
||||||
public AjaxResult voidBill(@RequestBody PayBillCommand command)
|
public AjaxResult voidBill(@RequestBody PayBillCommand command)
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package com.bms.price.pay;
|
package com.bms.price.pay;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import com.bms.price.domain.PayOrderCostResult;
|
import com.bms.price.domain.PayOrderCostResult;
|
||||||
|
import com.bms.price.domain.PayPostcodeGroup;
|
||||||
|
import com.bms.price.domain.PayPostcodeLine;
|
||||||
import com.bms.price.domain.PayProduct;
|
import com.bms.price.domain.PayProduct;
|
||||||
import com.bms.price.domain.PaySurcharge;
|
import com.bms.price.domain.PaySurcharge;
|
||||||
import com.bms.price.domain.PayWeightBand;
|
import com.bms.price.domain.PayWeightBand;
|
||||||
@@ -113,6 +116,133 @@ class PayCostCalculatorTest
|
|||||||
assertTrue(hasFee(result, "基础运费", "1.57"));
|
assertTrue(hasFee(result, "基础运费", "1.57"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void remoteZoneUsesMainlandFreightAndItsOwnSurcharge()
|
||||||
|
{
|
||||||
|
PayProduct product = new PayProduct();
|
||||||
|
product.setProductName("Evri 48H");
|
||||||
|
product.setCurrency("GBP");
|
||||||
|
product.setWeightBands(new ArrayList<>(List.of(
|
||||||
|
band("0", "1", "1.51", "UK Mainland"),
|
||||||
|
band("1", "2", "1.72", "UK Mainland"))));
|
||||||
|
PaySurcharge light = fee(1L, "偏远附加费", "5.95", "GBP", "ZONE", null);
|
||||||
|
light.setZoneName("Highlands& Islands");
|
||||||
|
light.setWeightMin(BigDecimal.ZERO);
|
||||||
|
light.setWeightMax(BigDecimal.ONE);
|
||||||
|
PaySurcharge heavy = fee(2L, "偏远附加费", "6.20", "GBP", "ZONE", null);
|
||||||
|
heavy.setZoneName("Highlands& Islands");
|
||||||
|
heavy.setWeightMin(BigDecimal.ONE);
|
||||||
|
heavy.setWeightMax(new BigDecimal("2"));
|
||||||
|
product.setSurcharges(List.of(light, heavy));
|
||||||
|
|
||||||
|
PayOrderCostResult remote = PayCostCalculator.calculate(product, new BigDecimal("0.5"),
|
||||||
|
null, null, null, "IV1 1AA", "Highlands& Islands", null, 1);
|
||||||
|
assertTrue(hasFee(remote, "基础运费", "1.51"));
|
||||||
|
assertTrue(hasFee(remote, "偏远附加费", "5.95"));
|
||||||
|
assertEquals(1, remote.getFees().stream().filter(fee -> "偏远附加费".equals(fee.getFeeName())).count());
|
||||||
|
|
||||||
|
PayOrderCostResult mainland = PayCostCalculator.calculate(product, new BigDecimal("0.5"),
|
||||||
|
null, null, null, "LL22 8PJ", "UK Mainland", null, 1);
|
||||||
|
assertTrue(hasFee(mainland, "基础运费", "1.51"));
|
||||||
|
assertTrue(mainland.getFees().stream().noneMatch(fee -> "偏远附加费".equals(fee.getFeeName())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void spanishPrefixChargesFirstWeightThenExtraKg()
|
||||||
|
{
|
||||||
|
PayProduct product = new PayProduct();
|
||||||
|
product.setProductName("CTT 48H");
|
||||||
|
product.setCurrency("EUR");
|
||||||
|
product.setWeightBands(new ArrayList<>(List.of(band("0", "5", "2.05", "马德里"))));
|
||||||
|
PaySurcharge first = fee(1L, "偏远附加费", "7.1", "EUR", "POSTCODE", "07");
|
||||||
|
first.setZoneName("Balearic Islands");
|
||||||
|
first.setPostcodeParam("07");
|
||||||
|
first.setWeightMin(BigDecimal.ZERO);
|
||||||
|
first.setWeightMax(BigDecimal.ONE);
|
||||||
|
PaySurcharge extra = fee(2L, "偏远附加费", "4", "EUR", "EXTRA_KG", "1");
|
||||||
|
extra.setZoneName("Balearic Islands");
|
||||||
|
extra.setPostcodeParam("07");
|
||||||
|
extra.setPriceUnit("EUR/KG");
|
||||||
|
product.setSurcharges(List.of(first, extra));
|
||||||
|
|
||||||
|
PayOrderCostResult light = PayCostCalculator.calculate(product, new BigDecimal("0.8"),
|
||||||
|
null, null, null, "07008", "马德里", null, 1);
|
||||||
|
assertTrue(hasFee(light, "基础运费", "2.05"));
|
||||||
|
assertTrue(hasFee(light, "偏远附加费", "7.1"));
|
||||||
|
assertEquals(1, light.getFees().stream().filter(fee -> "偏远附加费".equals(fee.getFeeName())).count());
|
||||||
|
|
||||||
|
PayOrderCostResult heavy = PayCostCalculator.calculate(product, new BigDecimal("3"),
|
||||||
|
null, null, null, "07008", "马德里", null, 1);
|
||||||
|
assertTrue(hasFee(heavy, "基础运费", "2.05"));
|
||||||
|
assertTrue(hasFee(heavy, "偏远附加费", "7.1"));
|
||||||
|
assertTrue(hasFee(heavy, "偏远附加费", "8"));
|
||||||
|
|
||||||
|
PayOrderCostResult mainland = PayCostCalculator.calculate(product, new BigDecimal("3"),
|
||||||
|
null, null, null, "28001", "马德里", null, 1);
|
||||||
|
assertTrue(mainland.getFees().stream().noneMatch(fee -> "偏远附加费".equals(fee.getFeeName())));
|
||||||
|
assertTrue(PaySurchargeRules.matchesPostcode("07,35,38,51,52,9", "35001"));
|
||||||
|
assertTrue(PaySurchargeRules.matchesPostcode("07,35,38,51,52,9", "9000"));
|
||||||
|
assertFalse(PaySurchargeRules.matchesPostcode("07,35,38,51,52,9", "28001"));
|
||||||
|
assertTrue(PaySurchargeRules.matchesPostcode("IV,HS,KA27-28", "KA27 8AA"));
|
||||||
|
assertEquals("07", PaySurchargeRules.remotePrefixes("Balearic Islands 07邮编开头"));
|
||||||
|
assertEquals("35,38", PaySurchargeRules.remotePrefixes("Canary Islands 35、38邮编开头"));
|
||||||
|
assertEquals("9", PaySurchargeRules.remotePrefixes("Portuguese Island 9邮编开头"));
|
||||||
|
assertEquals("", PaySurchargeRules.remotePrefixes("岛屿邮编地区"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void productPostcodeBookChoosesTheMatchingRemoteFee()
|
||||||
|
{
|
||||||
|
List<PayRemoteBook.Hit> hits = List.of(new PayRemoteBook.Hit("偏远附加费", new BigDecimal("1.80"), "", "Highlands"));
|
||||||
|
PayOrderCostResult result = PayCostCalculator.calculate(royalMail(), new BigDecimal("4"),
|
||||||
|
null, null, null, "IV1 1AA", null, null, 1, hits);
|
||||||
|
assertTrue(hasFee(result, "偏远地区附加费", "1.80"));
|
||||||
|
assertTrue(result.getFees().stream().noneMatch(fee -> "偏远地区附加费".equals(fee.getFeeName())
|
||||||
|
&& fee.getAmount().compareTo(new BigDecimal("1.00")) == 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void linkedRemoteBookWithoutAHitDoesNotUseThePriceCardList()
|
||||||
|
{
|
||||||
|
PayOrderCostResult result = PayCostCalculator.calculate(royalMail(), new BigDecimal("4"),
|
||||||
|
null, null, null, "IV1 1AA", null, null, 1, List.of());
|
||||||
|
assertTrue(result.getFees().stream().noneMatch(fee -> fee.getFeeName().contains("偏远")));
|
||||||
|
assertTrue(hasFee(result, "基础运费", "1.62"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void remoteBookChargesAManualIslandFeeAndPerKg()
|
||||||
|
{
|
||||||
|
PayProduct product = new PayProduct();
|
||||||
|
product.setProductName("德国DPD挂号-国际件");
|
||||||
|
product.setCurrency("EUR");
|
||||||
|
product.setWeightBands(new ArrayList<>(List.of(band("0", "5", "3.20", ""))));
|
||||||
|
PaySurcharge island = fee(31L, "岛屿附加费", "12", "EUR", "MANUAL", null);
|
||||||
|
PaySurcharge perKg = fee(32L, "偏远附加费", "0.35", "EUR", "MANUAL", null);
|
||||||
|
perKg.setPriceUnit("EUR/KG");
|
||||||
|
product.setSurcharges(List.of(island, perKg));
|
||||||
|
List<PayRemoteBook.Hit> hits = List.of(new PayRemoteBook.Hit("", null, "", "岛屿"));
|
||||||
|
PayOrderCostResult result = PayCostCalculator.calculate(product, new BigDecimal("2"),
|
||||||
|
null, null, null, "1234", null, null, 1, hits);
|
||||||
|
assertTrue(hasFee(result, "岛屿附加费", "12"));
|
||||||
|
assertTrue(hasFee(result, "偏远附加费", "0.70"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void remoteHitsFollowTheProductGroupAndCountry()
|
||||||
|
{
|
||||||
|
PayPostcodeGroup germany = group(44L, "西班牙GLS", "REMOTE", "", "DE");
|
||||||
|
PayPostcodeGroup france = group(45L, "西班牙GLS", "REMOTE", "", "FR");
|
||||||
|
PayPostcodeLine berlin = line(44L, "10115");
|
||||||
|
PayPostcodeLine paris = line(45L, "75001");
|
||||||
|
List<PayRemoteBook.Hit> de = PayRemoteBook.hits(List.of(germany, france), List.of(berlin, paris), "10115", "DE", "德国");
|
||||||
|
assertEquals(1, de.size());
|
||||||
|
List<PayRemoteBook.Hit> none = PayRemoteBook.hits(List.of(germany, france), List.of(berlin, paris), "28001", "ES", "西班牙");
|
||||||
|
assertTrue(none.isEmpty());
|
||||||
|
PayPostcodeGroup delivery = group(42L, "西班牙CTT", "", "", "ES");
|
||||||
|
assertTrue(PayRemoteBook.hits(List.of(delivery), List.of(line(42L, "28001")), "28001", "ES", "西班牙") == null);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void severalZonesCannotBePricedFromTheOrder()
|
void severalZonesCannotBePricedFromTheOrder()
|
||||||
{
|
{
|
||||||
@@ -192,6 +322,26 @@ class PayCostCalculatorTest
|
|||||||
return band;
|
return band;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static PayPostcodeGroup group(Long id, String quoteSheet, String coverage, String feeName, String country)
|
||||||
|
{
|
||||||
|
PayPostcodeGroup group = new PayPostcodeGroup();
|
||||||
|
group.setGroupId(id);
|
||||||
|
group.setQuoteSheet(quoteSheet);
|
||||||
|
group.setCoverage(coverage);
|
||||||
|
group.setFeeName(feeName);
|
||||||
|
group.setCountryCode(country);
|
||||||
|
group.setConfigKind("ZONE");
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PayPostcodeLine line(Long groupId, String postcode)
|
||||||
|
{
|
||||||
|
PayPostcodeLine line = new PayPostcodeLine();
|
||||||
|
line.setGroupId(groupId);
|
||||||
|
line.setPostcode(postcode);
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
private static PaySurcharge fee(Long id, String name, String amount, String currency, String rule, String param)
|
private static PaySurcharge fee(Long id, String name, String amount, String currency, String rule, String param)
|
||||||
{
|
{
|
||||||
PaySurcharge fee = new PaySurcharge();
|
PaySurcharge fee = new PaySurcharge();
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ class PayWorkbookParserTest
|
|||||||
@Test
|
@Test
|
||||||
void dictMatchReadsNamedSheetAndSkipsOthers() throws Exception
|
void dictMatchReadsNamedSheetAndSkipsOthers() throws Exception
|
||||||
{
|
{
|
||||||
Path file = Path.of("D:/payment/Evri 48H.xlsx");
|
Path file = priceCard("Evri 48H.xlsx");
|
||||||
Assumptions.assumeTrue(Files.exists(file));
|
Assumptions.assumeTrue(Files.exists(file));
|
||||||
PayPreview preview = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(), "Evri 48H",
|
PayPreview preview = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(), "Evri 48H",
|
||||||
List.of("Evri 48H", "DHL Paket"));
|
List.of("Evri 48H", "DHL Paket"));
|
||||||
@@ -213,6 +213,14 @@ class PayWorkbookParserTest
|
|||||||
assertEquals("Evri 48H", evri.getDictValue());
|
assertEquals("Evri 48H", evri.getDictValue());
|
||||||
assertEquals("Evri 48H", evri.getSheetName());
|
assertEquals("Evri 48H", evri.getSheetName());
|
||||||
assertFalse(evri.getWeightBands().isEmpty());
|
assertFalse(evri.getWeightBands().isEmpty());
|
||||||
|
assertTrue(evri.getWeightBands().stream().allMatch(band -> "派送费".equals(band.getChargeName())));
|
||||||
|
assertTrue(evri.getWeightBands().stream().noneMatch(band -> bandText(band).contains("偏远") || bandText(band).contains("Highlands")));
|
||||||
|
assertTrue(evri.getWeightBands().stream().anyMatch(band -> decimal(band, "1.51") && band.getZoneName().contains("Mainland")));
|
||||||
|
PaySurcharge highlands = evri.getSurcharges().stream()
|
||||||
|
.filter(fee -> "偏远附加费".equals(fee.getFeeName()) && fee.getZoneName().contains("Highlands") && decimalFee(fee, "5.95"))
|
||||||
|
.findFirst().orElseThrow();
|
||||||
|
assertEquals("ZONE", highlands.getCalcRule());
|
||||||
|
assertTrue(highlands.getWeightMax() != null && highlands.getWeightMax().compareTo(java.math.BigDecimal.ONE) == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -280,7 +288,7 @@ class PayWorkbookParserTest
|
|||||||
@Test
|
@Test
|
||||||
void royalMailReadsRemoteAreaTableAndSkipsRemarks() throws Exception
|
void royalMailReadsRemoteAreaTableAndSkipsRemarks() throws Exception
|
||||||
{
|
{
|
||||||
Path file = Path.of("D:/payment/英国RM.xlsx");
|
Path file = priceCard("英国RM.xlsx");
|
||||||
Assumptions.assumeTrue(Files.exists(file));
|
Assumptions.assumeTrue(Files.exists(file));
|
||||||
PayPreview preview = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(), "英国RM", List.of("英国RM"));
|
PayPreview preview = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(), "英国RM", List.of("英国RM"));
|
||||||
assertEquals(1, preview.getProducts().size());
|
assertEquals(1, preview.getProducts().size());
|
||||||
@@ -318,6 +326,21 @@ class PayWorkbookParserTest
|
|||||||
assertTrue(rm.getExtraLines().isEmpty());
|
assertTrue(rm.getExtraLines().isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void royalMail48hRemoteRowsMatchPostcodePrefixes() throws Exception
|
||||||
|
{
|
||||||
|
Path file = priceCard("Royal Mail 48H.xlsx");
|
||||||
|
Assumptions.assumeTrue(Files.exists(file));
|
||||||
|
PayProduct rm = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(), "Royal Mail 48H",
|
||||||
|
List.of("Royal Mail 48H")).getProducts().get(0);
|
||||||
|
List<PaySurcharge> remote = rm.getSurcharges().stream().filter(fee -> fee.getFeeName() != null && fee.getFeeName().contains("偏远")).toList();
|
||||||
|
assertFalse(remote.isEmpty(), rm.getSurcharges().stream().map(fee -> fee.getFeeName() + "/" + fee.getCalcRule() + "/" + fee.getCalcParam()).toList().toString());
|
||||||
|
assertTrue(remote.stream().allMatch(fee -> "POSTCODE".equals(fee.getCalcRule())),
|
||||||
|
remote.stream().map(fee -> fee.getFeeName() + "/" + fee.getCalcParam() + "/" + fee.getFeeDesc()).toList().toString());
|
||||||
|
assertTrue(remote.stream().anyMatch(fee -> fee.getCalcParam() != null && fee.getCalcParam().contains("IV") && !fee.getCalcParam().contains("邮编")));
|
||||||
|
assertTrue(rm.getWeightBands().stream().noneMatch(band -> bandText(band).contains("偏远")));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void evriOtherFeesKeepOneRowPerRegion() throws Exception
|
void evriOtherFeesKeepOneRowPerRegion() throws Exception
|
||||||
{
|
{
|
||||||
@@ -418,7 +441,7 @@ class PayWorkbookParserTest
|
|||||||
@Test
|
@Test
|
||||||
void ctt48hFileKeepsSurchargeTablesOutOfWeightBands() throws Exception
|
void ctt48hFileKeepsSurchargeTablesOutOfWeightBands() throws Exception
|
||||||
{
|
{
|
||||||
Path file = Path.of("D:/payment/CTT 48H.xlsx");
|
Path file = priceCard("CTT 48H.xlsx");
|
||||||
PayPreview preview = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(), "CTT 48H",
|
PayPreview preview = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(), "CTT 48H",
|
||||||
List.of("CTT 48H"));
|
List.of("CTT 48H"));
|
||||||
PayProduct ctt = preview.getProducts().get(0);
|
PayProduct ctt = preview.getProducts().get(0);
|
||||||
@@ -431,8 +454,17 @@ class PayWorkbookParserTest
|
|||||||
.filter(fee -> "偏远附加费".equals(fee.getFeeName()) && fee.getZoneName().contains("Balearic") && decimalFee(fee, "7.1"))
|
.filter(fee -> "偏远附加费".equals(fee.getFeeName()) && fee.getZoneName().contains("Balearic") && decimalFee(fee, "7.1"))
|
||||||
.findFirst().orElseThrow();
|
.findFirst().orElseThrow();
|
||||||
assertTrue(balearic.getFeeDesc().contains("首重"));
|
assertTrue(balearic.getFeeDesc().contains("首重"));
|
||||||
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> "偏远附加费".equals(fee.getFeeName())
|
assertEquals("POSTCODE", balearic.getCalcRule());
|
||||||
&& fee.getZoneName().contains("Canary") && decimalFee(fee, "4.5") && fee.getChargeText().contains("KG")));
|
assertTrue(balearic.getCalcParam() != null && balearic.getCalcParam().contains("07"));
|
||||||
|
assertTrue(balearic.getWeightMax() != null && balearic.getWeightMax().compareTo(new java.math.BigDecimal("1")) == 0);
|
||||||
|
PaySurcharge canary = ctt.getSurcharges().stream()
|
||||||
|
.filter(fee -> "偏远附加费".equals(fee.getFeeName()) && fee.getZoneName().contains("Canary") && decimalFee(fee, "4.5")
|
||||||
|
&& fee.getChargeText().contains("KG"))
|
||||||
|
.findFirst().orElseThrow();
|
||||||
|
assertEquals("EXTRA_KG", canary.getCalcRule());
|
||||||
|
assertEquals("1", canary.getCalcParam());
|
||||||
|
assertTrue(canary.getPostcodeParam() != null && canary.getPostcodeParam().contains("35") && canary.getPostcodeParam().contains("38"));
|
||||||
|
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> fee.getFeeName().contains("额外线路运输费") && "MANUAL".equals(fee.getCalcRule())));
|
||||||
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> fee.getFeeName().contains("额外线路运输费") && decimalFee(fee, "0.09")));
|
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> fee.getFeeName().contains("额外线路运输费") && decimalFee(fee, "0.09")));
|
||||||
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> fee.getFeeName().contains("额外线路运输费")
|
assertTrue(ctt.getSurcharges().stream().anyMatch(fee -> fee.getFeeName().contains("额外线路运输费")
|
||||||
&& fee.getFeeDesc().contains("800公里")));
|
&& fee.getFeeDesc().contains("800公里")));
|
||||||
@@ -442,7 +474,7 @@ class PayWorkbookParserTest
|
|||||||
@Test
|
@Test
|
||||||
void spainCttMainlandReadsBillableConcepts() throws Exception
|
void spainCttMainlandReadsBillableConcepts() throws Exception
|
||||||
{
|
{
|
||||||
Path file = Path.of("D:/payment/西班牙CTT带电专线(主要地区).xlsx");
|
Path file = priceCard("西班牙CTT带电专线(主要地区).xlsx");
|
||||||
Assumptions.assumeTrue(Files.exists(file));
|
Assumptions.assumeTrue(Files.exists(file));
|
||||||
PayPreview preview = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(),
|
PayPreview preview = parser.parseMatched(Files.readAllBytes(file), file.getFileName().toString(),
|
||||||
"西班牙CTT带电专线(主要地区)", List.of("西班牙CTT带电专线(主要地区)"));
|
"西班牙CTT带电专线(主要地区)", List.of("西班牙CTT带电专线(主要地区)"));
|
||||||
@@ -711,6 +743,16 @@ class PayWorkbookParserTest
|
|||||||
return fee.getAmount() != null && fee.getAmount().compareTo(new java.math.BigDecimal(amount)) == 0;
|
return fee.getAmount() != null && fee.getAmount().compareTo(new java.math.BigDecimal(amount)) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Path priceCard(String name)
|
||||||
|
{
|
||||||
|
Path current = Path.of("D:/A-payment").resolve(name);
|
||||||
|
if (Files.exists(current))
|
||||||
|
{
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
return Path.of("D:/payment").resolve(name);
|
||||||
|
}
|
||||||
|
|
||||||
private String bandText(PayWeightBand band)
|
private String bandText(PayWeightBand band)
|
||||||
{
|
{
|
||||||
return (band.getChargeName() == null ? "" : band.getChargeName()) + " "
|
return (band.getChargeName() == null ? "" : band.getChargeName()) + " "
|
||||||
|
|||||||
@@ -6,22 +6,30 @@ import lombok.Data;
|
|||||||
@Data
|
@Data
|
||||||
public class PayBillCommand
|
public class PayBillCommand
|
||||||
{
|
{
|
||||||
/** ORDER、TIME、LADING */
|
/** 取数方式:ORDER 订单号,TIME 计费日,LADING 提单号 */
|
||||||
private String sourceType;
|
private String sourceType;
|
||||||
|
|
||||||
|
/** 订单号原文,可用逗号、空格或换行分隔 */
|
||||||
private String orderText;
|
private String orderText;
|
||||||
|
|
||||||
|
/** 计费日起,yyyy-MM-dd。sourceType 为 TIME 时必填 */
|
||||||
private String beginDate;
|
private String beginDate;
|
||||||
|
|
||||||
|
/** 计费日止,yyyy-MM-dd,含当天 */
|
||||||
private String endDate;
|
private String endDate;
|
||||||
|
|
||||||
|
/** 提单号原文,可用逗号、空格或换行分隔 */
|
||||||
private String ladingText;
|
private String ladingText;
|
||||||
|
|
||||||
|
/** 供应商ID。生成账单时必填,预览不用 */
|
||||||
private Long supplierId;
|
private Long supplierId;
|
||||||
|
|
||||||
|
/** 账单备注 */
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|
||||||
|
/** 作废原因。作废时必填 */
|
||||||
private String voidReason;
|
private String voidReason;
|
||||||
|
|
||||||
|
/** 要作废的应付账单ID */
|
||||||
private Long billId;
|
private Long billId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,15 @@ import lombok.Data;
|
|||||||
@Data
|
@Data
|
||||||
public class PayBillPreview
|
public class PayBillPreview
|
||||||
{
|
{
|
||||||
|
/** 还没进未作废账单、可以生成的订单 */
|
||||||
private List<PayBillOrder> orders = new ArrayList<>();
|
private List<PayBillOrder> orders = new ArrayList<>();
|
||||||
|
|
||||||
|
/** 按订单号查询时没有找到的单号 */
|
||||||
private List<String> missingNos = new ArrayList<>();
|
private List<String> missingNos = new ArrayList<>();
|
||||||
|
|
||||||
|
/** 已经挂在未作废账单上的订单 */
|
||||||
private List<PayBillOrder> occupied = new ArrayList<>();
|
private List<PayBillOrder> occupied = new ArrayList<>();
|
||||||
|
|
||||||
|
/** 给页面的提示,例如未找到订单或超过单次票数 */
|
||||||
private String message;
|
private String message;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,11 +51,20 @@ public class PaySurcharge
|
|||||||
/** 固定收取、产生时收取、旺季期间 */
|
/** 固定收取、产生时收取、旺季期间 */
|
||||||
private String chargeTiming;
|
private String chargeTiming;
|
||||||
|
|
||||||
/** ALWAYS、POSTCODE、WEIGHT、SIZE、RETURN、EXTRA_KG、MANUAL */
|
/** ALWAYS、POSTCODE、ZONE、WEIGHT、SIZE、RETURN、EXTRA_KG、MANUAL */
|
||||||
private String calcRule;
|
private String calcRule;
|
||||||
|
|
||||||
/** 邮编列表、重量阈值或尺寸表达式 */
|
/** 邮编列表、重量阈值或尺寸表达式。续重时这里是公斤阈值 */
|
||||||
private String calcParam;
|
private String calcParam;
|
||||||
|
|
||||||
|
/** 偏远邮编规则。续重行的阈值在 calcParam,邮编放这里,避免互相覆盖 */
|
||||||
|
private String postcodeParam;
|
||||||
|
|
||||||
|
/** 适用重量下限,空表示不限。首重从 0 开始 */
|
||||||
|
private BigDecimal weightMin;
|
||||||
|
|
||||||
|
/** 适用重量上限,空表示不限。分区价按这个区间只收一档 */
|
||||||
|
private BigDecimal weightMax;
|
||||||
|
|
||||||
private Integer sortNo;
|
private Integer sortNo;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,26 +8,90 @@ import com.bms.price.domain.PayBillLog;
|
|||||||
import com.bms.price.domain.PayBillOrder;
|
import com.bms.price.domain.PayBillOrder;
|
||||||
import com.bms.price.domain.PrcPayBillFee;
|
import com.bms.price.domain.PrcPayBillFee;
|
||||||
|
|
||||||
|
/** 供应商应付账单 pay_bill 及其明细、日志。 */
|
||||||
public interface PayBillMapper
|
public interface PayBillMapper
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* 按账单号、供应商、状态、币种查询账单头。
|
||||||
|
*
|
||||||
|
* @param query 查询条件
|
||||||
|
* @return 账单头列表
|
||||||
|
*/
|
||||||
List<PayBill> selectPayBillList(PayBill query);
|
List<PayBill> selectPayBillList(PayBill query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按主键读取一张账单头。
|
||||||
|
*
|
||||||
|
* @param billId 应付账单ID
|
||||||
|
* @return 账单头,不存在时为 null
|
||||||
|
*/
|
||||||
PayBill selectPayBillById(Long billId);
|
PayBill selectPayBillById(Long billId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取一张账单的费用快照。
|
||||||
|
*
|
||||||
|
* @param billId 应付账单ID
|
||||||
|
* @return 费用行
|
||||||
|
*/
|
||||||
List<PayBillLine> selectLines(Long billId);
|
List<PayBillLine> selectLines(Long billId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取一张账单的操作日志。
|
||||||
|
*
|
||||||
|
* @param billId 应付账单ID
|
||||||
|
* @return 按写入顺序排列的日志
|
||||||
|
*/
|
||||||
List<PayBillLog> selectLogs(Long billId);
|
List<PayBillLog> selectLogs(Long billId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按订单号、计费日或提单号查找可出账订单,并标出已挂在未作废账单上的账单号。
|
||||||
|
*
|
||||||
|
* @param orderNos 订单号,空则不按订单号限制
|
||||||
|
* @param beginDate 计费日起,yyyy-MM-dd,空则不限制
|
||||||
|
* @param endDate 计费日止,含当天,空则不限制
|
||||||
|
* @param ladingNos 提单号,同时匹配订单上的航空主单号和应付提单
|
||||||
|
* @return 最多 2000 票订单
|
||||||
|
*/
|
||||||
List<PayBillOrder> selectCandidateOrders(@Param("orderNos") List<String> orderNos, @Param("beginDate") String beginDate,
|
List<PayBillOrder> selectCandidateOrders(@Param("orderNos") List<String> orderNos, @Param("beginDate") String beginDate,
|
||||||
@Param("endDate") String endDate, @Param("ladingNos") List<String> ladingNos);
|
@Param("endDate") String endDate, @Param("ladingNos") List<String> ladingNos);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取这些订单当前的应付费用,供生成账单时抄成快照。
|
||||||
|
*
|
||||||
|
* @param billIds 计费订单ID
|
||||||
|
* @return 订单应付费用
|
||||||
|
*/
|
||||||
List<PrcPayBillFee> selectOrderFees(@Param("billIds") List<String> billIds);
|
List<PrcPayBillFee> selectOrderFees(@Param("billIds") List<String> billIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入一张草稿应付账单。
|
||||||
|
*
|
||||||
|
* @param bill 账单头,账单号和供应商由调用方填好
|
||||||
|
* @return 影响行数
|
||||||
|
*/
|
||||||
int insertPayBill(PayBill bill);
|
int insertPayBill(PayBill bill);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新账单状态。作废时同时写原因、作废人和作废时间。
|
||||||
|
*
|
||||||
|
* @param bill 至少包含账单ID、状态和更新人
|
||||||
|
* @return 影响行数
|
||||||
|
*/
|
||||||
int updatePayBillStatus(PayBill bill);
|
int updatePayBillStatus(PayBill bill);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量写入账单费用快照。
|
||||||
|
*
|
||||||
|
* @param list 费用行,每行已带账单ID
|
||||||
|
* @return 影响行数
|
||||||
|
*/
|
||||||
int insertLines(@Param("list") List<PayBillLine> list);
|
int insertLines(@Param("list") List<PayBillLine> list);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入一条账单操作日志。
|
||||||
|
*
|
||||||
|
* @param log 操作类型、前后状态和说明
|
||||||
|
* @return 影响行数
|
||||||
|
*/
|
||||||
int insertLog(PayBillLog log);
|
int insertLog(PayBillLog log);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ public interface PayPostcodeMapper
|
|||||||
|
|
||||||
List<PayPostcodeGroup> selectGroupsByTokens(@Param("tokens") List<String> tokens);
|
List<PayPostcodeGroup> selectGroupsByTokens(@Param("tokens") List<String> tokens);
|
||||||
|
|
||||||
|
/** 报价表、销售产品或工作表与产品名称完全一致的邮编配置 */
|
||||||
|
List<PayPostcodeGroup> selectGroupsForProduct(@Param("names") List<String> names);
|
||||||
|
|
||||||
List<PayPostcodeLine> selectRegionKeys(@Param("groupIds") List<Long> groupIds);
|
List<PayPostcodeLine> selectRegionKeys(@Param("groupIds") List<Long> groupIds);
|
||||||
|
|
||||||
List<PayPostcodeLine> selectLinesByGroups(@Param("groupIds") List<Long> groupIds);
|
List<PayPostcodeLine> selectLinesByGroups(@Param("groupIds") List<Long> groupIds);
|
||||||
|
|||||||
@@ -137,4 +137,7 @@ public interface PrcOrderBillMapper
|
|||||||
|
|
||||||
/** 只更新应付合计,不改应收。 */
|
/** 只更新应付合计,不改应收。 */
|
||||||
int updatePayTotal(@Param("billId") String billId, @Param("payTotal") BigDecimal payTotal);
|
int updatePayTotal(@Param("billId") String billId, @Param("payTotal") BigDecimal payTotal);
|
||||||
|
|
||||||
|
/** Count order bills that reference this sales package version. */
|
||||||
|
int countByPsvId(@Param("psvId") Long psvId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,38 @@ import java.util.List;
|
|||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
import com.bms.price.domain.PrcPayBillFee;
|
import com.bms.price.domain.PrcPayBillFee;
|
||||||
|
|
||||||
/** 应付账单费用 mapper */
|
/** 订单应付费用 prc_pay_bill_fee。 */
|
||||||
public interface PrcPayBillFeeMapper
|
public interface PrcPayBillFeeMapper
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* 读取一票订单的全部应付费用,运费在前,附加费在后。
|
||||||
|
*
|
||||||
|
* @param billId 计费订单ID
|
||||||
|
* @return 费用明细
|
||||||
|
*/
|
||||||
public List<PrcPayBillFee> selectByBillId(String billId);
|
public List<PrcPayBillFee> selectByBillId(String billId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入一条订单应付费用。
|
||||||
|
*
|
||||||
|
* @param fee 费用类型、名称、金额和币种
|
||||||
|
* @return 影响行数
|
||||||
|
*/
|
||||||
public int insertPrcPayBillFee(PrcPayBillFee fee);
|
public int insertPrcPayBillFee(PrcPayBillFee fee);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量插入订单应付费用。
|
||||||
|
*
|
||||||
|
* @param fees 本次要保存的费用
|
||||||
|
* @return 影响行数
|
||||||
|
*/
|
||||||
public int insertBatch(@Param("list") List<PrcPayBillFee> fees);
|
public int insertBatch(@Param("list") List<PrcPayBillFee> fees);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除一票订单上来源为计算的费用。提单分摊和人工费用保留。
|
||||||
|
*
|
||||||
|
* @param billId 计费订单ID
|
||||||
|
* @return 影响行数
|
||||||
|
*/
|
||||||
public int deleteByBillId(String billId);
|
public int deleteByBillId(String billId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,54 @@ package com.bms.price.mapper;
|
|||||||
|
|
||||||
import com.bms.price.domain.PrcPayBill;
|
import com.bms.price.domain.PrcPayBill;
|
||||||
|
|
||||||
/** prc_pay_bill mapper */
|
/** 订单应付主表 prc_pay_bill。一票计费订单对应一行。 */
|
||||||
public interface PrcPayBillMapper
|
public interface PrcPayBillMapper
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* 按计费订单读取应付主表。
|
||||||
|
*
|
||||||
|
* @param billId 计费订单ID
|
||||||
|
* @return 订单应付,没有时为 null
|
||||||
|
*/
|
||||||
PrcPayBill selectByBillId(String billId);
|
PrcPayBill selectByBillId(String billId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按应付主表ID读取。
|
||||||
|
*
|
||||||
|
* @param payBillId 订单应付ID
|
||||||
|
* @return 订单应付,没有时为 null
|
||||||
|
*/
|
||||||
PrcPayBill selectByPayBillId(String payBillId);
|
PrcPayBill selectByPayBillId(String payBillId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增一票订单的应付主表。
|
||||||
|
*
|
||||||
|
* @param row 应付合计、币种和业务员
|
||||||
|
* @return 影响行数
|
||||||
|
*/
|
||||||
int insertPrcPayBill(PrcPayBill row);
|
int insertPrcPayBill(PrcPayBill row);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新已有订单应付的合计和业务员。
|
||||||
|
*
|
||||||
|
* @param row 含应付主表ID的更新内容
|
||||||
|
* @return 影响行数
|
||||||
|
*/
|
||||||
int updatePrcPayBill(PrcPayBill row);
|
int updatePrcPayBill(PrcPayBill row);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按计费订单插入或更新应付主表。
|
||||||
|
*
|
||||||
|
* @param row 订单应付
|
||||||
|
* @return 影响行数
|
||||||
|
*/
|
||||||
int upsertPrcPayBill(PrcPayBill row);
|
int upsertPrcPayBill(PrcPayBill row);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除一票订单的应付主表。
|
||||||
|
*
|
||||||
|
* @param billId 计费订单ID
|
||||||
|
* @return 影响行数
|
||||||
|
*/
|
||||||
int deleteByBillId(String billId);
|
int deleteByBillId(String billId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,14 +28,62 @@ public final class PayCostCalculator
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按重量、尺寸和邮编计算一票尾程应付。不指定地区,也不传入偏远命中结果。
|
||||||
|
*
|
||||||
|
* @param product 当前价卡版本,含重量段和附加费
|
||||||
|
* @param weight 计费重,公斤
|
||||||
|
* @param length 长,厘米
|
||||||
|
* @param width 宽,厘米
|
||||||
|
* @param height 高,厘米
|
||||||
|
* @param postcode 收件邮编
|
||||||
|
* @param bizDate 取价日,用来判断附加费是否在有效期内
|
||||||
|
* @param orderStatus 订单状态,退件时才收退件类附加费
|
||||||
|
* @return 费用明细和算不了的附加费说明
|
||||||
|
*/
|
||||||
public static PayOrderCostResult calculate(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
|
public static PayOrderCostResult calculate(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
|
||||||
BigDecimal height, String postcode, Date bizDate, Integer orderStatus)
|
BigDecimal height, String postcode, Date bizDate, Integer orderStatus)
|
||||||
{
|
{
|
||||||
return calculate(product, weight, length, width, height, postcode, null, bizDate, orderStatus);
|
return calculate(product, weight, length, width, height, postcode, null, bizDate, orderStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在已经选定价卡地区时计算尾程应付。偏远仍按价卡上的邮编条件判断。
|
||||||
|
*
|
||||||
|
* @param product 当前价卡版本
|
||||||
|
* @param weight 计费重,公斤
|
||||||
|
* @param length 长,厘米
|
||||||
|
* @param width 宽,厘米
|
||||||
|
* @param height 高,厘米
|
||||||
|
* @param postcode 收件邮编
|
||||||
|
* @param zoneName 邮编对上的价卡地区,空表示不按地区过滤
|
||||||
|
* @param bizDate 取价日
|
||||||
|
* @param orderStatus 订单状态
|
||||||
|
* @return 费用明细和算不了的附加费说明
|
||||||
|
*/
|
||||||
public static PayOrderCostResult calculate(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
|
public static PayOrderCostResult calculate(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
|
||||||
BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus)
|
BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus)
|
||||||
|
{
|
||||||
|
return calculate(product, weight, length, width, height, postcode, zoneName, bizDate, orderStatus, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算一票尾程应付。remoteHits 为空列表表示产品有偏远邮编但这次没命中;为 null 表示没有这份配置,仍按价卡上的邮编条件。
|
||||||
|
*
|
||||||
|
* @param product 当前价卡版本
|
||||||
|
* @param weight 计费重,公斤
|
||||||
|
* @param length 长,厘米
|
||||||
|
* @param width 宽,厘米
|
||||||
|
* @param height 高,厘米
|
||||||
|
* @param postcode 收件邮编
|
||||||
|
* @param zoneName 价卡地区
|
||||||
|
* @param bizDate 取价日
|
||||||
|
* @param orderStatus 订单状态
|
||||||
|
* @param remoteHits 偏远邮编命中结果
|
||||||
|
* @return 费用明细和算不了的附加费说明
|
||||||
|
*/
|
||||||
|
public static PayOrderCostResult calculate(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
|
||||||
|
BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus, List<PayRemoteBook.Hit> remoteHits)
|
||||||
{
|
{
|
||||||
PayOrderCostResult result = new PayOrderCostResult();
|
PayOrderCostResult result = new PayOrderCostResult();
|
||||||
result.setFees(new ArrayList<>());
|
result.setFees(new ArrayList<>());
|
||||||
@@ -52,7 +100,12 @@ public final class PayCostCalculator
|
|||||||
result.setMessage("订单没有计费重");
|
result.setMessage("订单没有计费重");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
List<PayWeightBand> bands = forZone(usableBands(product.getWeightBands()), zoneName);
|
List<PayWeightBand> usable = usableBands(product.getWeightBands());
|
||||||
|
List<PayWeightBand> bands = forZone(usable, zoneName);
|
||||||
|
if (zoneName != null && !zoneName.isBlank() && bands.isEmpty() && hasZoneFee(product, zoneName))
|
||||||
|
{
|
||||||
|
bands = deliveryBands(usable, product);
|
||||||
|
}
|
||||||
if (zoneName != null && !zoneName.isBlank() && bands.isEmpty())
|
if (zoneName != null && !zoneName.isBlank() && bands.isEmpty())
|
||||||
{
|
{
|
||||||
result.setMessage("邮编对应的地区「" + zoneName + "」不在价卡重量段里");
|
result.setMessage("邮编对应的地区「" + zoneName + "」不在价卡重量段里");
|
||||||
@@ -88,7 +141,7 @@ public final class PayCostCalculator
|
|||||||
result.getFees().add(freightLine);
|
result.getFees().add(freightLine);
|
||||||
result.setCurrency(currency);
|
result.setCurrency(currency);
|
||||||
addSurcharges(product, weight, length, width, height, postcode, zoneName, bizDate, orderStatus, freight, currency,
|
addSurcharges(product, weight, length, width, height, postcode, zoneName, bizDate, orderStatus, freight, currency,
|
||||||
band.getSectionName(), result);
|
band.getSectionName(), remoteHits, result);
|
||||||
BigDecimal total = BigDecimal.ZERO;
|
BigDecimal total = BigDecimal.ZERO;
|
||||||
for (PayCostLine fee : result.getFees())
|
for (PayCostLine fee : result.getFees())
|
||||||
{
|
{
|
||||||
@@ -99,6 +152,79 @@ public final class PayCostCalculator
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 邮编落到偏远分区,而价卡重量段只有大陆派送费时,运费用大陆档。 */
|
||||||
|
private static boolean hasZoneFee(PayProduct product, String zoneName)
|
||||||
|
{
|
||||||
|
if (product.getSurcharges() == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (PaySurcharge fee : product.getSurcharges())
|
||||||
|
{
|
||||||
|
if (fee != null && PaySurchargeRules.ZONE.equals(fee.getCalcRule()) && PayZoneMatcher.sameZone(fee.getZoneName(), zoneName))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PayWeightBand> deliveryBands(List<PayWeightBand> bands, PayProduct product)
|
||||||
|
{
|
||||||
|
List<String> remoteZones = new ArrayList<>();
|
||||||
|
if (product.getSurcharges() != null)
|
||||||
|
{
|
||||||
|
for (PaySurcharge fee : product.getSurcharges())
|
||||||
|
{
|
||||||
|
if (fee != null && PaySurchargeRules.ZONE.equals(fee.getCalcRule()) && fee.getZoneName() != null && !fee.getZoneName().isBlank())
|
||||||
|
{
|
||||||
|
remoteZones.add(fee.getZoneName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<PayWeightBand> kept = new ArrayList<>();
|
||||||
|
for (PayWeightBand band : bands)
|
||||||
|
{
|
||||||
|
boolean remote = false;
|
||||||
|
for (String zone : remoteZones)
|
||||||
|
{
|
||||||
|
if (PayZoneMatcher.sameZone(band.getZoneName(), zone))
|
||||||
|
{
|
||||||
|
remote = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!remote)
|
||||||
|
{
|
||||||
|
kept.add(band);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return kept;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 首重对更重的包裹仍然收取,超出部分由续重另计。 */
|
||||||
|
private static boolean weightAtLeast(PaySurcharge fee, BigDecimal weight)
|
||||||
|
{
|
||||||
|
if (fee.getWeightMin() == null || weight == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return weight.compareTo(fee.getWeightMin()) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean weightIn(PaySurcharge fee, BigDecimal weight)
|
||||||
|
{
|
||||||
|
if (weight == null || (fee.getWeightMin() == null && fee.getWeightMax() == null))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (fee.getWeightMin() != null && weight.compareTo(fee.getWeightMin()) < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return fee.getWeightMax() == null || weight.compareTo(fee.getWeightMax()) <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
private static List<PayWeightBand> forZone(List<PayWeightBand> bands, String zoneName)
|
private static List<PayWeightBand> forZone(List<PayWeightBand> bands, String zoneName)
|
||||||
{
|
{
|
||||||
if (zoneName == null || zoneName.isBlank())
|
if (zoneName == null || zoneName.isBlank())
|
||||||
@@ -118,7 +244,7 @@ public final class PayCostCalculator
|
|||||||
|
|
||||||
private static void addSurcharges(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
|
private static void addSurcharges(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
|
||||||
BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus, BigDecimal freight, String currency,
|
BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus, BigDecimal freight, String currency,
|
||||||
String freightSection, PayOrderCostResult result)
|
String freightSection, List<PayRemoteBook.Hit> remoteHits, PayOrderCostResult result)
|
||||||
{
|
{
|
||||||
if (product.getSurcharges() == null)
|
if (product.getSurcharges() == null)
|
||||||
{
|
{
|
||||||
@@ -131,24 +257,59 @@ public final class PayCostCalculator
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (zoneName != null && !zoneName.isBlank() && fee.getZoneName() != null && !fee.getZoneName().isBlank()
|
|
||||||
&& !PayZoneMatcher.sameZone(fee.getZoneName(), zoneName))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String rule = fee.getCalcRule() == null || fee.getCalcRule().isBlank() ? PaySurchargeRules.MANUAL : fee.getCalcRule();
|
String rule = fee.getCalcRule() == null || fee.getCalcRule().isBlank() ? PaySurchargeRules.MANUAL : fee.getCalcRule();
|
||||||
String name = blank(fee.getFeeName());
|
if (remoteHits != null && remoteFee(fee))
|
||||||
if (PaySurchargeRules.POSTCODE.equals(rule))
|
|
||||||
{
|
{
|
||||||
|
String name = blank(fee.getFeeName());
|
||||||
if (postcode == null || postcode.isBlank())
|
if (postcode == null || postcode.isBlank())
|
||||||
{
|
{
|
||||||
result.getSkipped().add(name + ":订单没有邮编");
|
result.getSkipped().add(name + ":订单没有邮编");
|
||||||
}
|
}
|
||||||
else if (PaySurchargeRules.matchesPostcode(fee.getCalcParam(), postcode))
|
else if (bookApplies(remoteHits, fee, rule))
|
||||||
|
{
|
||||||
|
String remark = "邮编 " + postcode.trim();
|
||||||
|
if (PaySurchargeRules.EXTRA_KG.equals(rule))
|
||||||
|
{
|
||||||
|
addExtraKg(result.getFees(), fee, weight, currency);
|
||||||
|
}
|
||||||
|
else if (perKg(fee))
|
||||||
|
{
|
||||||
|
addPerKg(result.getFees(), fee, weight, currency, remark);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
addFlat(result.getFees(), fee, currency, remark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
boolean postcodeDriven = PaySurchargeRules.POSTCODE.equals(rule) || !blank(fee.getPostcodeParam()).isEmpty();
|
||||||
|
if (!postcodeDriven && zoneName != null && !zoneName.isBlank() && fee.getZoneName() != null && !fee.getZoneName().isBlank()
|
||||||
|
&& !PayZoneMatcher.sameZone(fee.getZoneName(), zoneName))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String name = blank(fee.getFeeName());
|
||||||
|
if (PaySurchargeRules.POSTCODE.equals(rule))
|
||||||
|
{
|
||||||
|
String param = blank(fee.getPostcodeParam()).isEmpty() ? fee.getCalcParam() : fee.getPostcodeParam();
|
||||||
|
if (postcode == null || postcode.isBlank())
|
||||||
|
{
|
||||||
|
result.getSkipped().add(name + ":订单没有邮编");
|
||||||
|
}
|
||||||
|
else if (PaySurchargeRules.matchesPostcode(param, postcode) && weightAtLeast(fee, weight))
|
||||||
{
|
{
|
||||||
addFlat(result.getFees(), fee, currency, "邮编 " + postcode.trim());
|
addFlat(result.getFees(), fee, currency, "邮编 " + postcode.trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (PaySurchargeRules.ZONE.equals(rule))
|
||||||
|
{
|
||||||
|
boolean already = result.getFees().stream().anyMatch(line -> name.equals(line.getFeeName()));
|
||||||
|
if (!already && zoneName != null && !zoneName.isBlank() && PayZoneMatcher.sameZone(fee.getZoneName(), zoneName) && weightIn(fee, weight))
|
||||||
|
{
|
||||||
|
addFlat(result.getFees(), fee, currency, "地区 " + zoneName.replace('\n', ' '));
|
||||||
|
}
|
||||||
|
}
|
||||||
else if (PaySurchargeRules.WEIGHT.equals(rule))
|
else if (PaySurchargeRules.WEIGHT.equals(rule))
|
||||||
{
|
{
|
||||||
if (PaySurchargeRules.matchesWeight(fee.getCalcParam(), weight))
|
if (PaySurchargeRules.matchesWeight(fee.getCalcParam(), weight))
|
||||||
@@ -180,7 +341,18 @@ public final class PayCostCalculator
|
|||||||
}
|
}
|
||||||
else if (PaySurchargeRules.EXTRA_KG.equals(rule))
|
else if (PaySurchargeRules.EXTRA_KG.equals(rule))
|
||||||
{
|
{
|
||||||
if (sameSection(fee.getSectionName(), freightSection))
|
if (postcodeDriven)
|
||||||
|
{
|
||||||
|
if (postcode == null || postcode.isBlank())
|
||||||
|
{
|
||||||
|
result.getSkipped().add(name + ":订单没有邮编");
|
||||||
|
}
|
||||||
|
else if (PaySurchargeRules.matchesPostcode(fee.getPostcodeParam(), postcode))
|
||||||
|
{
|
||||||
|
addExtraKg(result.getFees(), fee, weight, currency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (sameSection(fee.getSectionName(), freightSection))
|
||||||
{
|
{
|
||||||
addExtraKg(result.getFees(), fee, weight, currency);
|
addExtraKg(result.getFees(), fee, weight, currency);
|
||||||
}
|
}
|
||||||
@@ -278,7 +450,8 @@ public final class PayCostCalculator
|
|||||||
}
|
}
|
||||||
for (PaySurcharge fee : product.getSurcharges())
|
for (PaySurcharge fee : product.getSurcharges())
|
||||||
{
|
{
|
||||||
if (fee == null || !PaySurchargeRules.EXTRA_KG.equals(fee.getCalcRule()) || fee.getAmount() == null)
|
if (fee == null || !PaySurchargeRules.EXTRA_KG.equals(fee.getCalcRule()) || fee.getAmount() == null
|
||||||
|
|| !blank(fee.getPostcodeParam()).isEmpty())
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -319,6 +492,90 @@ public final class PayCostCalculator
|
|||||||
return top;
|
return top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean remoteFee(PaySurcharge fee)
|
||||||
|
{
|
||||||
|
String name = blank(fee.getFeeName()).toLowerCase(Locale.ROOT);
|
||||||
|
return name.contains("偏远") || name.contains("岛屿") || name.contains("remote") || name.contains("island");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 邮编配置命中后,金额相同的那一档才收;分区写明时还要对上地区。 */
|
||||||
|
private static boolean bookApplies(List<PayRemoteBook.Hit> hits, PaySurcharge fee, String rule)
|
||||||
|
{
|
||||||
|
boolean extra = PaySurchargeRules.EXTRA_KG.equals(rule);
|
||||||
|
boolean zoned = !blank(fee.getZoneName()).isEmpty();
|
||||||
|
for (PayRemoteBook.Hit hit : hits)
|
||||||
|
{
|
||||||
|
if (hit == null || !sameFeeName(hit.getFeeName(), fee.getFeeName()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
boolean amountKnown = hit.getAmount() != null && fee.getAmount() != null;
|
||||||
|
if (amountKnown && hit.getAmount().compareTo(fee.getAmount()) != 0 && !extra)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
boolean areaKnown = !blank(hit.getZoneName()).isEmpty() || !blank(hit.getRegionName()).isEmpty();
|
||||||
|
if (zoned && areaKnown && !sameArea(hit, fee))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (extra && zoned && !sameArea(hit, fee))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (extra && amountKnown && hit.getAmount().compareTo(fee.getAmount()) != 0 && !sameArea(hit, fee))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean sameFeeName(String bookName, String feeName)
|
||||||
|
{
|
||||||
|
String book = feeKey(bookName);
|
||||||
|
String fee = feeKey(feeName);
|
||||||
|
if (book.isEmpty())
|
||||||
|
{
|
||||||
|
return fee.contains("偏远") || fee.contains("岛屿") || fee.contains("remote") || fee.contains("island");
|
||||||
|
}
|
||||||
|
return fee.contains(book) || book.contains(fee);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String feeKey(String name)
|
||||||
|
{
|
||||||
|
return blank(name).toLowerCase(Locale.ROOT).replace("地区", "").replace(" ", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean sameArea(PayRemoteBook.Hit hit, PaySurcharge fee)
|
||||||
|
{
|
||||||
|
return PayZoneMatcher.sameZone(fee.getZoneName(), hit.getZoneName())
|
||||||
|
|| PayZoneMatcher.sameZone(fee.getZoneName(), hit.getRegionName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean perKg(PaySurcharge fee)
|
||||||
|
{
|
||||||
|
String unit = blank(fee.getPriceUnit()).toUpperCase(Locale.ROOT);
|
||||||
|
return unit.contains("KG") || unit.contains("公斤");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addPerKg(List<PayCostLine> fees, PaySurcharge fee, BigDecimal weight, String currency, String remark)
|
||||||
|
{
|
||||||
|
if (fee.getAmount() == null || weight == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String feeCurrency = normalize(fee.getCurrency());
|
||||||
|
if (!feeCurrency.isEmpty() && !feeCurrency.equals(currency))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BigDecimal amount = fee.getAmount().multiply(weight).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
long scId = fee.getSurchargeId() == null ? 0L : fee.getSurchargeId();
|
||||||
|
fees.add(line("SURCHARGE", blank(fee.getFeeName()), scId, amount, currency, remark + " × " + plain(weight) + "kg"));
|
||||||
|
}
|
||||||
|
|
||||||
private static void addFlat(List<PayCostLine> fees, PaySurcharge fee, String currency, String remark)
|
private static void addFlat(List<PayCostLine> fees, PaySurcharge fee, String currency, String remark)
|
||||||
{
|
{
|
||||||
PayCostLine line = matchedFee(fee, null, currency, remark);
|
PayCostLine line = matchedFee(fee, null, currency, remark);
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
package com.bms.price.pay;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import com.bms.price.domain.PayPostcodeGroup;
|
||||||
|
import com.bms.price.domain.PayPostcodeLine;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 产品在应付邮编配置里的偏远名单。报价表或销售产品与价卡一致时,这份名单决定是否加偏远附加费。
|
||||||
|
*/
|
||||||
|
public final class PayRemoteBook
|
||||||
|
{
|
||||||
|
private static final Pattern AMOUNT = Pattern.compile("(\\d+(?:\\.\\d+)?)");
|
||||||
|
|
||||||
|
private PayRemoteBook()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 命中的一条偏远邮编,带上这一组的费用名和金额。 */
|
||||||
|
public static final class Hit
|
||||||
|
{
|
||||||
|
private final String feeName;
|
||||||
|
|
||||||
|
private final BigDecimal amount;
|
||||||
|
|
||||||
|
private final String zoneName;
|
||||||
|
|
||||||
|
private final String regionName;
|
||||||
|
|
||||||
|
public Hit(String feeName, BigDecimal amount, String zoneName, String regionName)
|
||||||
|
{
|
||||||
|
this.feeName = feeName == null ? "" : feeName;
|
||||||
|
this.amount = amount;
|
||||||
|
this.zoneName = zoneName == null ? "" : zoneName;
|
||||||
|
this.regionName = regionName == null ? "" : regionName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFeeName()
|
||||||
|
{
|
||||||
|
return feeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getAmount()
|
||||||
|
{
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getZoneName()
|
||||||
|
{
|
||||||
|
return zoneName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRegionName()
|
||||||
|
{
|
||||||
|
return regionName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean remoteGroup(PayPostcodeGroup group)
|
||||||
|
{
|
||||||
|
if (group == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String coverage = group.getCoverage() == null ? "" : group.getCoverage().trim();
|
||||||
|
if ("REMOTE".equalsIgnoreCase(coverage))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ("REACH".equalsIgnoreCase(coverage) || "UNREACH".equalsIgnoreCase(coverage))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String fee = group.getFeeName() == null ? "" : group.getFeeName();
|
||||||
|
return fee.contains("偏远") || fee.toLowerCase(Locale.ROOT).contains("remote");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 没有偏远配置时返回 null,沿用价卡上的邮编条件。
|
||||||
|
* 有配置但邮编没命中时返回空列表,不加偏远附加费。
|
||||||
|
*/
|
||||||
|
public static List<Hit> hits(List<PayPostcodeGroup> groups, List<PayPostcodeLine> lines, String postcode,
|
||||||
|
String countryCode, String countryName)
|
||||||
|
{
|
||||||
|
if (groups == null || groups.isEmpty())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
boolean anyRemote = false;
|
||||||
|
Map<Long, PayPostcodeGroup> remote = new LinkedHashMap<>();
|
||||||
|
for (PayPostcodeGroup group : groups)
|
||||||
|
{
|
||||||
|
if (group == null || group.getGroupId() == null || !remoteGroup(group))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
anyRemote = true;
|
||||||
|
if (sameCountry(group, countryCode, countryName))
|
||||||
|
{
|
||||||
|
remote.put(group.getGroupId(), group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!anyRemote)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (remote.isEmpty())
|
||||||
|
{
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
if (postcode == null || postcode.isBlank() || lines == null || lines.isEmpty())
|
||||||
|
{
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<Hit> hits = new ArrayList<>();
|
||||||
|
for (PayPostcodeLine line : lines)
|
||||||
|
{
|
||||||
|
if (line == null || !remote.containsKey(line.getGroupId()) || !PayZoneMatcher.matchesLine(line, postcode))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
PayPostcodeGroup group = remote.get(line.getGroupId());
|
||||||
|
String region = first(line.getRegionName(), line.getRegionNameCn());
|
||||||
|
hits.add(new Hit(group.getFeeName(), amountOf(group.getFeeText()), line.getZoneName(), region));
|
||||||
|
}
|
||||||
|
return hits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BigDecimal amountOf(String feeText)
|
||||||
|
{
|
||||||
|
if (feeText == null || feeText.isBlank())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Matcher matcher = AMOUNT.matcher(feeText);
|
||||||
|
if (!matcher.find())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new BigDecimal(matcher.group(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean sameCountry(PayPostcodeGroup group, String countryCode, String countryName)
|
||||||
|
{
|
||||||
|
boolean orderBlank = blank(countryCode) && blank(countryName);
|
||||||
|
boolean groupBlank = blank(group.getCountryCode()) && blank(group.getCountryName());
|
||||||
|
if (orderBlank || groupBlank)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!blank(countryCode) && countryCode.equalsIgnoreCase(group.getCountryCode().trim()))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !blank(countryName) && countryName.trim().equals(group.getCountryName() == null ? "" : group.getCountryName().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String first(String left, String right)
|
||||||
|
{
|
||||||
|
if (left != null && !left.isBlank())
|
||||||
|
{
|
||||||
|
return left.trim();
|
||||||
|
}
|
||||||
|
return right == null ? "" : right.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean blank(String value)
|
||||||
|
{
|
||||||
|
return value == null || value.isBlank();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,11 @@ public final class PaySurchargeRules
|
|||||||
/** 超过重量段上限后,按超出的公斤数乘单价 */
|
/** 超过重量段上限后,按超出的公斤数乘单价 */
|
||||||
public static final String EXTRA_KG = "EXTRA_KG";
|
public static final String EXTRA_KG = "EXTRA_KG";
|
||||||
|
|
||||||
|
/** 邮编已经落到这个分区,再按重量区间收取,例如英国高地 */
|
||||||
|
public static final String ZONE = "ZONE";
|
||||||
|
|
||||||
|
private static final Pattern REMOTE_PREFIX = Pattern.compile("(\\d{1,5}(?:\\s*[、,,]\\s*\\d{1,5})*)\\s*邮编");
|
||||||
|
|
||||||
private static final Pattern RANGE = Pattern.compile("([A-Z]{1,2})(\\d{1,2})\\s*-\\s*(\\d{1,2})");
|
private static final Pattern RANGE = Pattern.compile("([A-Z]{1,2})(\\d{1,2})\\s*-\\s*(\\d{1,2})");
|
||||||
|
|
||||||
private static final Pattern DISTRICT = Pattern.compile("([A-Z]{1,2})(\\d{1,2})");
|
private static final Pattern DISTRICT = Pattern.compile("([A-Z]{1,2})(\\d{1,2})");
|
||||||
@@ -107,14 +112,24 @@ public final class PaySurchargeRules
|
|||||||
|
|
||||||
public static boolean matchesPostcode(String param, String postcode)
|
public static boolean matchesPostcode(String param, String postcode)
|
||||||
{
|
{
|
||||||
UkPostcode parsed = parseUk(postcode);
|
if (param == null || param.isBlank() || postcode == null || postcode.isBlank())
|
||||||
if (parsed == null || param == null || param.isBlank())
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
UkPostcode parsed = parseUk(postcode);
|
||||||
|
String digits = postcode.replaceAll("\\D", "");
|
||||||
for (String token : param.split(","))
|
for (String token : param.split(","))
|
||||||
{
|
{
|
||||||
if (tokenMatches(token.trim().toUpperCase(Locale.ROOT), parsed))
|
String value = token.trim().toUpperCase(Locale.ROOT);
|
||||||
|
if (value.isEmpty())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (parsed != null && tokenMatches(value, parsed))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (parsed == null && value.matches("\\d{1,5}") && !digits.isEmpty() && digits.startsWith(value))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -122,6 +137,28 @@ public final class PaySurchargeRules
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 从“07邮编开头”“35、38邮编开头”取出数字前缀。没有邮编字样时不猜。 */
|
||||||
|
public static String remotePrefixes(String text)
|
||||||
|
{
|
||||||
|
if (text == null || !text.contains("邮编"))
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
List<String> tokens = new ArrayList<>();
|
||||||
|
Matcher matcher = REMOTE_PREFIX.matcher(text);
|
||||||
|
while (matcher.find())
|
||||||
|
{
|
||||||
|
for (String part : matcher.group(1).split("[、,,\\s]+"))
|
||||||
|
{
|
||||||
|
if (part.matches("\\d{1,5}") && !tokens.contains(part))
|
||||||
|
{
|
||||||
|
tokens.add(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return String.join(",", tokens);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean matchesWeight(String param, BigDecimal weight)
|
public static boolean matchesWeight(String param, BigDecimal weight)
|
||||||
{
|
{
|
||||||
if (param == null || param.isBlank() || weight == null)
|
if (param == null || param.isBlank() || weight == null)
|
||||||
@@ -212,7 +249,7 @@ public final class PaySurchargeRules
|
|||||||
}
|
}
|
||||||
int cut = Math.max(desc.lastIndexOf(';'), desc.lastIndexOf(';'));
|
int cut = Math.max(desc.lastIndexOf(';'), desc.lastIndexOf(';'));
|
||||||
String source = cut >= 0 ? desc.substring(cut + 1) : desc;
|
String source = cut >= 0 ? desc.substring(cut + 1) : desc;
|
||||||
String upper = source.toUpperCase(Locale.ROOT).replace(',', ',').replace('、', ',').replace('.', ',');
|
String upper = source.replace("邮编开头", " ").toUpperCase(Locale.ROOT).replace(',', ',').replace('、', ',').replace('.', ',');
|
||||||
List<String> tokens = new ArrayList<>();
|
List<String> tokens = new ArrayList<>();
|
||||||
for (String raw : upper.split("[,\\s]+"))
|
for (String raw : upper.split("[,\\s]+"))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -454,6 +454,9 @@ public class PayWorkbookParser
|
|||||||
copy.setChargeTiming(fee.getChargeTiming());
|
copy.setChargeTiming(fee.getChargeTiming());
|
||||||
copy.setCalcRule(fee.getCalcRule());
|
copy.setCalcRule(fee.getCalcRule());
|
||||||
copy.setCalcParam(fee.getCalcParam());
|
copy.setCalcParam(fee.getCalcParam());
|
||||||
|
copy.setPostcodeParam(fee.getPostcodeParam());
|
||||||
|
copy.setWeightMin(fee.getWeightMin());
|
||||||
|
copy.setWeightMax(fee.getWeightMax());
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1250,6 +1253,7 @@ public class PayWorkbookParser
|
|||||||
kind[col] = columnKind(head, numericBelow(grid, col, data));
|
kind[col] = columnKind(head, numericBelow(grid, col, data));
|
||||||
}
|
}
|
||||||
int added = product.getSurcharges().size();
|
int added = product.getSurcharges().size();
|
||||||
|
Map<String, BigDecimal> includedKg = new LinkedHashMap<>();
|
||||||
while (data < grid.rows())
|
while (data < grid.rows())
|
||||||
{
|
{
|
||||||
if (grid.blankRow(data) || isHardHeader(grid, data))
|
if (grid.blankRow(data) || isHardHeader(grid, data))
|
||||||
@@ -1296,8 +1300,7 @@ public class PayWorkbookParser
|
|||||||
String desc = country.isBlank() ? weight : country + ";" + weight;
|
String desc = country.isBlank() ? weight : country + ";" + weight;
|
||||||
addSurcharge(product, feeName, feeName, zone, standard.trim(), desc, "");
|
addSurcharge(product, feeName, feeName, zone, standard.trim(), desc, "");
|
||||||
PaySurcharge fee = product.getSurcharges().get(product.getSurcharges().size() - 1);
|
PaySurcharge fee = product.getSurcharges().get(product.getSurcharges().size() - 1);
|
||||||
fee.setCalcRule(PaySurchargeRules.MANUAL);
|
applyFeeRateRule(fee, feeName, zone, weight, includedKg);
|
||||||
fee.setCalcParam(weight);
|
|
||||||
}
|
}
|
||||||
if (!priced && joinRow(grid, data).length() > 40)
|
if (!priced && joinRow(grid, data).length() > 40)
|
||||||
{
|
{
|
||||||
@@ -1452,6 +1455,11 @@ public class PayWorkbookParser
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
priced = true;
|
priced = true;
|
||||||
|
if (isSurchargePriceHead(parent[col]))
|
||||||
|
{
|
||||||
|
addPricedSurcharge(product, blankTo(section, "价格"), parent[col], zoneOf(sub[col], parent[col]), weight, price);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
boolean feeHead = isPriceHead(parent[col]);
|
boolean feeHead = isPriceHead(parent[col]);
|
||||||
PayWeightBand band = new PayWeightBand();
|
PayWeightBand band = new PayWeightBand();
|
||||||
band.setSectionName(blankTo(section, "价格"));
|
band.setSectionName(blankTo(section, "价格"));
|
||||||
@@ -4156,6 +4164,60 @@ public class PayWorkbookParser
|
|||||||
|| lower.contains("line haul");
|
|| lower.contains("line haul");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 偏远行按邮编前缀计费:首重整段收取,每增加只收超出部分。没有邮编列表的费用保持人工。
|
||||||
|
*/
|
||||||
|
private static void applyFeeRateRule(PaySurcharge fee, String feeName, String zone, String weight, Map<String, BigDecimal> includedKg)
|
||||||
|
{
|
||||||
|
boolean remote = feeName != null && feeName.contains("偏远");
|
||||||
|
boolean step = weight != null && (weight.contains("每增加") || weight.toLowerCase(Locale.ROOT).contains("additional"));
|
||||||
|
BigDecimal[] bounds = PayTexts.weightBounds(weight);
|
||||||
|
String prefixes = PaySurchargeRules.remotePrefixes(zone);
|
||||||
|
if (!step && bounds[1] != null)
|
||||||
|
{
|
||||||
|
includedKg.put(zone, bounds[1]);
|
||||||
|
}
|
||||||
|
BigDecimal included = includedKg.get(zone);
|
||||||
|
if (remote && step && !prefixes.isEmpty())
|
||||||
|
{
|
||||||
|
fee.setCalcRule(PaySurchargeRules.EXTRA_KG);
|
||||||
|
fee.setCalcParam(included == null ? "1" : included.stripTrailingZeros().toPlainString());
|
||||||
|
fee.setPostcodeParam(prefixes);
|
||||||
|
fee.setWeightMin(null);
|
||||||
|
fee.setWeightMax(null);
|
||||||
|
}
|
||||||
|
else if (remote && !prefixes.isEmpty())
|
||||||
|
{
|
||||||
|
fee.setCalcRule(PaySurchargeRules.POSTCODE);
|
||||||
|
fee.setCalcParam(prefixes);
|
||||||
|
fee.setPostcodeParam(prefixes);
|
||||||
|
fee.setWeightMin(bounds[0]);
|
||||||
|
fee.setWeightMax(bounds[1]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fee.setCalcRule(PaySurchargeRules.MANUAL);
|
||||||
|
fee.setCalcParam(weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 价格表里和派送费并排的偏远列,按分区和重量区间记附加费,不进重量段。 */
|
||||||
|
private void addPricedSurcharge(PayProduct product, String section, String head, String zone, String weight, BigDecimal price)
|
||||||
|
{
|
||||||
|
String feeName = shortCharge(head);
|
||||||
|
String standard = price.stripTrailingZeros().toPlainString() + " " + feeUnit(head, weight);
|
||||||
|
addSurcharge(product, section, feeName, zone, standard.trim(), weight, "");
|
||||||
|
PaySurcharge fee = product.getSurcharges().get(product.getSurcharges().size() - 1);
|
||||||
|
BigDecimal[] bounds = PayTexts.weightBounds(weight);
|
||||||
|
if (bounds[0] != null && bounds[1] != null)
|
||||||
|
{
|
||||||
|
fee.setWeightMin(bounds[0]);
|
||||||
|
fee.setWeightMax(bounds[1]);
|
||||||
|
fee.setCalcRule(PaySurchargeRules.ZONE);
|
||||||
|
fee.setCalcParam(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static String feeUnit(String head, String weight)
|
private static String feeUnit(String head, String weight)
|
||||||
{
|
{
|
||||||
String unit = unitOf(head + " " + weight);
|
String unit = unitOf(head + " " + weight);
|
||||||
|
|||||||
@@ -88,6 +88,11 @@ public final class PayZoneMatcher
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean matchesLine(PayPostcodeLine line, String postcode)
|
||||||
|
{
|
||||||
|
return line != null && matchedLength(line, postcode) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
public static Decision resolve(List<String> zoneNames, List<PayPostcodeLine> lines, String postcode)
|
public static Decision resolve(List<String> zoneNames, List<PayPostcodeLine> lines, String postcode)
|
||||||
{
|
{
|
||||||
if (zoneNames == null || zoneNames.size() <= 1)
|
if (zoneNames == null || zoneNames.size() <= 1)
|
||||||
|
|||||||
@@ -5,19 +5,65 @@ import com.bms.price.domain.PayBill;
|
|||||||
import com.bms.price.domain.PayBillCommand;
|
import com.bms.price.domain.PayBillCommand;
|
||||||
import com.bms.price.domain.PayBillPreview;
|
import com.bms.price.domain.PayBillPreview;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应付账单。查询、预览、生成,以及确认、付款、作废。
|
||||||
|
*/
|
||||||
public interface IPayBillService
|
public interface IPayBillService
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* 按条件查询应付账单列表。
|
||||||
|
*
|
||||||
|
* @param query 账单号、供应商、状态、币种,空着的条件不过滤
|
||||||
|
* @return 账单头列表
|
||||||
|
*/
|
||||||
List<PayBill> selectPayBillList(PayBill query);
|
List<PayBill> selectPayBillList(PayBill query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询一张应付账单,带上费用明细和操作日志。
|
||||||
|
*
|
||||||
|
* @param billId 应付账单ID
|
||||||
|
* @return 账单详情
|
||||||
|
*/
|
||||||
PayBill selectPayBillDetail(Long billId);
|
PayBill selectPayBillDetail(Long billId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预览可生成的订单。不重算费用,也不写账单。
|
||||||
|
*
|
||||||
|
* @param command 查询方式,以及订单号、计费日起止或提单号
|
||||||
|
* @return 可生成订单、未找到的单号、已经入账的订单
|
||||||
|
*/
|
||||||
PayBillPreview preview(PayBillCommand command);
|
PayBillPreview preview(PayBillCommand command);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成应付账单。重算范围内订单的尾程应付,再把订单费用抄成账单明细。
|
||||||
|
* 已经挂在未作废账单上的订单不会进入本次账单。
|
||||||
|
*
|
||||||
|
* @param command 供应商和取数范围
|
||||||
|
* @return 草稿账单,含明细和生成日志
|
||||||
|
*/
|
||||||
PayBill generate(PayBillCommand command);
|
PayBill generate(PayBillCommand command);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把草稿确认为已确认。
|
||||||
|
*
|
||||||
|
* @param billId 应付账单ID
|
||||||
|
* @return 成功时为 1
|
||||||
|
*/
|
||||||
int confirm(Long billId);
|
int confirm(Long billId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把已确认账单登记为已付款。
|
||||||
|
*
|
||||||
|
* @param billId 应付账单ID
|
||||||
|
* @return 成功时为 1
|
||||||
|
*/
|
||||||
int markPaid(Long billId);
|
int markPaid(Long billId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作废账单。已付款的不能作废,作废后数据仍保留。
|
||||||
|
*
|
||||||
|
* @param command 账单ID和作废原因
|
||||||
|
* @return 成功时为 1
|
||||||
|
*/
|
||||||
int voidBill(PayBillCommand command);
|
int voidBill(PayBillCommand command);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,16 @@ package com.bms.price.service;
|
|||||||
|
|
||||||
import com.bms.price.domain.PayOrderCostResult;
|
import com.bms.price.domain.PayOrderCostResult;
|
||||||
|
|
||||||
/** 按订单生成应付:渠道服务商上的应付产品,用订单基础数据计算。 */
|
/**
|
||||||
|
* 按订单计算尾程应付。用渠道上的应付产品和订单重量、尺寸、邮编取价,并写入订单应付。
|
||||||
|
*/
|
||||||
public interface IPayOrderCostService
|
public interface IPayOrderCostService
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* 重算一票订单的尾程应付,覆盖原来的计算费用。
|
||||||
|
*
|
||||||
|
* @param billId 计费订单ID,对应 prc_order_bill.bill_id
|
||||||
|
* @return 本次算出的产品、版本、币种和费用明细
|
||||||
|
*/
|
||||||
PayOrderCostResult generate(String billId);
|
PayOrderCostResult generate(String billId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,12 +48,24 @@ public class PayBillServiceImpl implements IPayBillService
|
|||||||
@Autowired
|
@Autowired
|
||||||
private BmsSupplierMapper supplierMapper;
|
private BmsSupplierMapper supplierMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按账单号、供应商、状态、币种查询应付账单。
|
||||||
|
*
|
||||||
|
* @param query 查询条件,空字段不参与过滤
|
||||||
|
* @return 账单头列表
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public List<PayBill> selectPayBillList(PayBill query)
|
public List<PayBill> selectPayBillList(PayBill query)
|
||||||
{
|
{
|
||||||
return payBillMapper.selectPayBillList(query);
|
return payBillMapper.selectPayBillList(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询一张应付账单,并装上费用明细和操作日志。
|
||||||
|
*
|
||||||
|
* @param billId 应付账单ID
|
||||||
|
* @return 账单详情
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public PayBill selectPayBillDetail(Long billId)
|
public PayBill selectPayBillDetail(Long billId)
|
||||||
{
|
{
|
||||||
@@ -67,12 +79,25 @@ public class PayBillServiceImpl implements IPayBillService
|
|||||||
return bill;
|
return bill;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预览可生成的订单。不重算尾程,也不写账单。
|
||||||
|
*
|
||||||
|
* @param command 查询方式,以及订单号、计费日或提单号
|
||||||
|
* @return 可生成订单、未找到的单号、已经入账的订单
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public PayBillPreview preview(PayBillCommand command)
|
public PayBillPreview preview(PayBillCommand command)
|
||||||
{
|
{
|
||||||
return collect(command, false);
|
return collect(command, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成应付账单。校验供应商后,对未入账订单重算尾程,再把订单费用抄成账单明细。
|
||||||
|
* 所选订单币种必须一致。账单以草稿写入,并记一条生成日志。
|
||||||
|
*
|
||||||
|
* @param command 供应商和取数范围。sourceType 为 ORDER、TIME 或 LADING
|
||||||
|
* @return 刚生成的草稿账单,含明细和日志
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public PayBill generate(PayBillCommand command)
|
public PayBill generate(PayBillCommand command)
|
||||||
@@ -165,6 +190,12 @@ public class PayBillServiceImpl implements IPayBillService
|
|||||||
return selectPayBillDetail(bill.getBillId());
|
return selectPayBillDetail(bill.getBillId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把草稿账单改为已确认。
|
||||||
|
*
|
||||||
|
* @param billId 应付账单ID
|
||||||
|
* @return 成功时为 1
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public int confirm(Long billId)
|
public int confirm(Long billId)
|
||||||
@@ -182,6 +213,12 @@ public class PayBillServiceImpl implements IPayBillService
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把已确认账单登记为已付款。
|
||||||
|
*
|
||||||
|
* @param billId 应付账单ID
|
||||||
|
* @return 成功时为 1
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public int markPaid(Long billId)
|
public int markPaid(Long billId)
|
||||||
@@ -199,6 +236,12 @@ public class PayBillServiceImpl implements IPayBillService
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作废账单。必须填写作废原因。已作废的不能再作废,已付款的不能直接作废。
|
||||||
|
*
|
||||||
|
* @param command 账单ID和作废原因
|
||||||
|
* @return 成功时为 1
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public int voidBill(PayBillCommand command)
|
public int voidBill(PayBillCommand command)
|
||||||
@@ -232,6 +275,13 @@ public class PayBillServiceImpl implements IPayBillService
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按订单号、计费日或提单号查出订单,并分成可生成和已入账两组。
|
||||||
|
*
|
||||||
|
* @param command 查询方式和对应的单号或日期
|
||||||
|
* @param forGenerate 为 true 时不把未找到的单号写进提示,留给生成时抛错
|
||||||
|
* @return 预览结果。可生成订单放在 orders,已入账订单放在 occupied
|
||||||
|
*/
|
||||||
private PayBillPreview collect(PayBillCommand command, boolean forGenerate)
|
private PayBillPreview collect(PayBillCommand command, boolean forGenerate)
|
||||||
{
|
{
|
||||||
String mode = mode(command);
|
String mode = mode(command);
|
||||||
@@ -301,11 +351,23 @@ public class PayBillServiceImpl implements IPayBillService
|
|||||||
return preview;
|
return preview;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取出还没有挂在未作废账单上的订单。
|
||||||
|
*
|
||||||
|
* @param preview 预览结果
|
||||||
|
* @return 可以进入本次账单的订单
|
||||||
|
*/
|
||||||
private List<PayBillOrder> freeOrders(PayBillPreview preview)
|
private List<PayBillOrder> freeOrders(PayBillPreview preview)
|
||||||
{
|
{
|
||||||
return preview.getOrders();
|
return preview.getOrders();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取这批订单里第一个非空币种。都没有时用 CNY,后面仍会按费用行再核对。
|
||||||
|
*
|
||||||
|
* @param orders 本次要出账的订单
|
||||||
|
* @return 币种代码
|
||||||
|
*/
|
||||||
private String currencyOf(List<PayBillOrder> orders)
|
private String currencyOf(List<PayBillOrder> orders)
|
||||||
{
|
{
|
||||||
String currency = "";
|
String currency = "";
|
||||||
@@ -320,6 +382,13 @@ public class PayBillServiceImpl implements IPayBillService
|
|||||||
return currency.isEmpty() ? "CNY" : currency;
|
return currency.isEmpty() ? "CNY" : currency;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用计费订单ID找到对应订单,用来回填订单号和提单号。
|
||||||
|
*
|
||||||
|
* @param orders 本次订单
|
||||||
|
* @param billId 计费订单ID
|
||||||
|
* @return 订单;对不上时返回 null
|
||||||
|
*/
|
||||||
private PayBillOrder findOrder(List<PayBillOrder> orders, String billId)
|
private PayBillOrder findOrder(List<PayBillOrder> orders, String billId)
|
||||||
{
|
{
|
||||||
for (PayBillOrder order : orders)
|
for (PayBillOrder order : orders)
|
||||||
@@ -332,6 +401,12 @@ public class PayBillServiceImpl implements IPayBillService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取账单头。账单不存在时抛出业务异常。
|
||||||
|
*
|
||||||
|
* @param billId 应付账单ID
|
||||||
|
* @return 账单头
|
||||||
|
*/
|
||||||
private PayBill required(Long billId)
|
private PayBill required(Long billId)
|
||||||
{
|
{
|
||||||
PayBill bill = payBillMapper.selectPayBillById(billId);
|
PayBill bill = payBillMapper.selectPayBillById(billId);
|
||||||
@@ -342,6 +417,16 @@ public class PayBillServiceImpl implements IPayBillService
|
|||||||
return bill;
|
return bill;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写一条应付账单操作日志。
|
||||||
|
*
|
||||||
|
* @param billId 应付账单ID
|
||||||
|
* @param type 操作类型:CREATE 生成、CONFIRM 确认、PAY 付款、VOID 作废
|
||||||
|
* @param from 变更前状态,新建时为空
|
||||||
|
* @param to 变更后状态
|
||||||
|
* @param desc 操作说明
|
||||||
|
* @param operator 操作人账号
|
||||||
|
*/
|
||||||
private void writeLog(Long billId, String type, String from, String to, String desc, String operator)
|
private void writeLog(Long billId, String type, String from, String to, String desc, String operator)
|
||||||
{
|
{
|
||||||
PayBillLog log = new PayBillLog();
|
PayBillLog log = new PayBillLog();
|
||||||
@@ -354,12 +439,23 @@ public class PayBillServiceImpl implements IPayBillService
|
|||||||
payBillMapper.insertLog(log);
|
payBillMapper.insertLog(log);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成应付账单号。格式为 AP、年月、8 位随机码。
|
||||||
|
*
|
||||||
|
* @return 账单号
|
||||||
|
*/
|
||||||
private String nextNo()
|
private String nextNo()
|
||||||
{
|
{
|
||||||
return "AP" + new SimpleDateFormat("yyyyMM").format(new Date())
|
return "AP" + new SimpleDateFormat("yyyyMM").format(new Date())
|
||||||
+ IdUtils.fastSimpleUUID().substring(0, 8).toUpperCase();
|
+ IdUtils.fastSimpleUUID().substring(0, 8).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把 yyyy-MM-dd 转成日期。空文本返回 null。
|
||||||
|
*
|
||||||
|
* @param text 计费日起或止
|
||||||
|
* @return 日期
|
||||||
|
*/
|
||||||
private Date parseDay(String text)
|
private Date parseDay(String text)
|
||||||
{
|
{
|
||||||
if (text == null || text.isBlank())
|
if (text == null || text.isBlank())
|
||||||
@@ -370,6 +466,12 @@ public class PayBillServiceImpl implements IPayBillService
|
|||||||
return java.sql.Date.valueOf(day);
|
return java.sql.Date.valueOf(day);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取查询方式,并转成大写。
|
||||||
|
*
|
||||||
|
* @param command 预览或生成参数
|
||||||
|
* @return ORDER 订单号、TIME 计费日、LADING 提单号
|
||||||
|
*/
|
||||||
private String mode(PayBillCommand command)
|
private String mode(PayBillCommand command)
|
||||||
{
|
{
|
||||||
if (command == null || command.getSourceType() == null || command.getSourceType().isBlank())
|
if (command == null || command.getSourceType() == null || command.getSourceType().isBlank())
|
||||||
@@ -379,6 +481,12 @@ public class PayBillServiceImpl implements IPayBillService
|
|||||||
return command.getSourceType().trim().toUpperCase(Locale.ROOT);
|
return command.getSourceType().trim().toUpperCase(Locale.ROOT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把订单号或提单号按逗号、顿号、分号、空白拆开,并去掉重复和空白。
|
||||||
|
*
|
||||||
|
* @param text 页面上粘贴的单号原文
|
||||||
|
* @return 去重后的单号
|
||||||
|
*/
|
||||||
static List<String> tokens(String text)
|
static List<String> tokens(String text)
|
||||||
{
|
{
|
||||||
LinkedHashSet<String> values = new LinkedHashSet<>();
|
LinkedHashSet<String> values = new LinkedHashSet<>();
|
||||||
|
|||||||
+233
-19
@@ -1,11 +1,8 @@
|
|||||||
package com.bms.price.service.impl;
|
package com.bms.price.service.impl;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -18,6 +15,7 @@ import com.bms.price.domain.PayOrderCostResult;
|
|||||||
import com.bms.price.domain.PayPostcodeGroup;
|
import com.bms.price.domain.PayPostcodeGroup;
|
||||||
import com.bms.price.domain.PayPostcodeLine;
|
import com.bms.price.domain.PayPostcodeLine;
|
||||||
import com.bms.price.domain.PayProduct;
|
import com.bms.price.domain.PayProduct;
|
||||||
|
import com.bms.price.domain.PaySurcharge;
|
||||||
import com.bms.price.domain.PayWeightBand;
|
import com.bms.price.domain.PayWeightBand;
|
||||||
import com.bms.price.domain.PrcOrderBill;
|
import com.bms.price.domain.PrcOrderBill;
|
||||||
import com.bms.price.domain.PrcOrderBillExt;
|
import com.bms.price.domain.PrcOrderBillExt;
|
||||||
@@ -29,6 +27,8 @@ import com.bms.price.mapper.PrcOrderBillMapper;
|
|||||||
import com.bms.price.mapper.PrcPayBillFeeMapper;
|
import com.bms.price.mapper.PrcPayBillFeeMapper;
|
||||||
import com.bms.price.mapper.PrcPayBillMapper;
|
import com.bms.price.mapper.PrcPayBillMapper;
|
||||||
import com.bms.price.pay.PayCostCalculator;
|
import com.bms.price.pay.PayCostCalculator;
|
||||||
|
import com.bms.price.pay.PayRemoteBook;
|
||||||
|
import com.bms.price.pay.PaySurchargeRules;
|
||||||
import com.bms.price.pay.PayZoneMatcher;
|
import com.bms.price.pay.PayZoneMatcher;
|
||||||
import com.bms.price.service.IPayOrderCostService;
|
import com.bms.price.service.IPayOrderCostService;
|
||||||
import com.bms.price.service.IPayProductService;
|
import com.bms.price.service.IPayProductService;
|
||||||
@@ -60,6 +60,13 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService
|
|||||||
@Autowired
|
@Autowired
|
||||||
private PayPostcodeMapper payPostcodeMapper;
|
private PayPostcodeMapper payPostcodeMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重算一票订单的尾程应付并落库。
|
||||||
|
* 先找渠道和计费日对应的价卡版本,再用重量、尺寸、邮编算出费用,写回订单应付。
|
||||||
|
*
|
||||||
|
* @param billId 计费订单ID
|
||||||
|
* @return 本次使用的渠道、价卡版本和费用明细
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public PayOrderCostResult generate(String billId)
|
public PayOrderCostResult generate(String billId)
|
||||||
@@ -75,13 +82,16 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService
|
|||||||
}
|
}
|
||||||
PrcOrderBillExt ext = orderBillExtMapper.selectByBillId(bill.getBillId());
|
PrcOrderBillExt ext = orderBillExtMapper.selectByBillId(bill.getBillId());
|
||||||
ServiceChannel channel = resolveChannel(ext);
|
ServiceChannel channel = resolveChannel(ext);
|
||||||
java.util.Date bizDate = bill.getBizDate() != null ? bill.getBizDate() : bill.getOrderTime();
|
Date bizDate = bill.getBizDate() != null ? bill.getBizDate() : bill.getOrderTime();
|
||||||
PayProduct product = payProductService.selectPriceForDate(channel.getId(), bizDate);
|
PayProduct product = payProductService.selectPriceForDate(channel.getId(), bizDate);
|
||||||
BigDecimal weight = firstPositive(bill.getChargeWeight(), bill.getWeight(), bill.getActualWeight(), bill.getReceiveWeight());
|
BigDecimal weight = firstPositive(bill.getChargeWeight(), bill.getWeight(), bill.getActualWeight(), bill.getReceiveWeight());
|
||||||
Integer orderStatus = bill.getOrderStatus() != null ? bill.getOrderStatus() : (ext == null ? null : ext.getOrderStatus());
|
Integer orderStatus = bill.getOrderStatus() != null ? bill.getOrderStatus() : (ext == null ? null : ext.getOrderStatus());
|
||||||
String zoneName = resolveZone(channel, product, bill.getDestPostcode(), bill.getCountryCode(), bill.getCountryName());
|
List<PayPostcodeGroup> linked = linkedGroups(product);
|
||||||
|
String zoneName = resolveZone(channel, product, bill.getDestPostcode(), bill.getCountryCode(), bill.getCountryName(), linked);
|
||||||
|
// 根据邮编判断是否是偏远地区
|
||||||
|
List<PayRemoteBook.Hit> remoteHits = remoteHits(linked, bill.getDestPostcode(), bill.getCountryCode(), bill.getCountryName());
|
||||||
PayOrderCostResult result = PayCostCalculator.calculate(product, weight, bill.getPkgLength(), bill.getPkgWidth(),
|
PayOrderCostResult result = PayCostCalculator.calculate(product, weight, bill.getPkgLength(), bill.getPkgWidth(),
|
||||||
bill.getPkgHeight(), bill.getDestPostcode(), zoneName, bizDate, orderStatus);
|
bill.getPkgHeight(), bill.getDestPostcode(), zoneName, bizDate, orderStatus, remoteHits);
|
||||||
result.setBillId(bill.getBillId());
|
result.setBillId(bill.getBillId());
|
||||||
result.setChannelId(channel.getId());
|
result.setChannelId(channel.getId());
|
||||||
result.setChannelName(channel.getChannelName());
|
result.setChannelName(channel.getChannelName());
|
||||||
@@ -101,14 +111,109 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 价卡分成多个地区时,用邮编在对应分区表里选出价格列。 */
|
/**
|
||||||
private String resolveZone(ServiceChannel channel, PayProduct product, String postcode, String countryCode, String countryName)
|
* 用产品名称、报价工作表、字典值和关联分区表,对上这份产品的邮编配置。
|
||||||
|
*
|
||||||
|
* @param product 当前价卡版本
|
||||||
|
* @return 名称能对上的邮编组,没有时为空列表
|
||||||
|
*/
|
||||||
|
private List<PayPostcodeGroup> linkedGroups(PayProduct product)
|
||||||
|
{
|
||||||
|
List<String> names = new ArrayList<>();
|
||||||
|
addName(names, product.getProductName());
|
||||||
|
addName(names, product.getSheetName());
|
||||||
|
addName(names, product.getDictValue());
|
||||||
|
addName(names, product.getRelatedSheet());
|
||||||
|
if (names.isEmpty())
|
||||||
|
{
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<PayPostcodeGroup> found = payPostcodeMapper.selectGroupsForProduct(names);
|
||||||
|
return found == null ? new ArrayList<>() : found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把非空名称放进匹配列表,已有的不重复加入。
|
||||||
|
*
|
||||||
|
* @param names 已经收集的名称
|
||||||
|
* @param value 产品名、工作表名或字典值
|
||||||
|
*/
|
||||||
|
private static void addName(List<String> names, String value)
|
||||||
|
{
|
||||||
|
if (value == null || value.isBlank())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String name = value.trim();
|
||||||
|
if (!names.contains(name))
|
||||||
|
{
|
||||||
|
names.add(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用产品自己的偏远邮编判断这票是否偏远。没有偏远配置时返回 null,计费改看价卡上的邮编条件。
|
||||||
|
*
|
||||||
|
* @param linked 已对上的邮编组
|
||||||
|
* @param postcode 收件邮编
|
||||||
|
* @param countryCode 目的国二字码
|
||||||
|
* @param countryName 目的国名称
|
||||||
|
* @return 命中的偏远分区;有配置但没命中时返回空列表
|
||||||
|
*/
|
||||||
|
private List<PayRemoteBook.Hit> remoteHits(List<PayPostcodeGroup> linked, String postcode, String countryCode, String countryName)
|
||||||
|
{
|
||||||
|
List<Long> remoteIds = new ArrayList<>();
|
||||||
|
boolean anyRemote = false;
|
||||||
|
for (PayPostcodeGroup group : linked)
|
||||||
|
{
|
||||||
|
if (group == null || group.getGroupId() == null || !PayRemoteBook.remoteGroup(group))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
anyRemote = true;
|
||||||
|
if (sameCountry(group, countryCode, countryName))
|
||||||
|
{
|
||||||
|
remoteIds.add(group.getGroupId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!anyRemote)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (remoteIds.isEmpty())
|
||||||
|
{
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<PayPostcodeLine> lines = postcode == null || postcode.isBlank()
|
||||||
|
? new ArrayList<>() : payPostcodeMapper.selectLinesByGroups(remoteIds);
|
||||||
|
return PayRemoteBook.hits(linked, lines, postcode, countryCode, countryName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 价卡有多个地区时,选出这一票对应的价格列。优先用产品关联的邮编分区,对不上再按名称关键字找。
|
||||||
|
* 只有一个地区时返回 null,计费不再按分区过滤。
|
||||||
|
*
|
||||||
|
* @param channel 订单渠道,关键字匹配时备用
|
||||||
|
* @param product 当前价卡版本
|
||||||
|
* @param postcode 收件邮编
|
||||||
|
* @param countryCode 目的国二字码
|
||||||
|
* @param countryName 目的国名称
|
||||||
|
* @param linked 已对上的邮编组
|
||||||
|
* @return 价卡上的地区名
|
||||||
|
*/
|
||||||
|
private String resolveZone(ServiceChannel channel, PayProduct product, String postcode, String countryCode, String countryName,
|
||||||
|
List<PayPostcodeGroup> linked)
|
||||||
{
|
{
|
||||||
List<String> zoneNames = zoneNames(product);
|
List<String> zoneNames = zoneNames(product);
|
||||||
if (zoneNames.size() <= 1)
|
if (zoneNames.size() <= 1)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
List<Long> linkedZoneIds = zoneGroupIds(linked, countryCode, countryName);
|
||||||
|
if (!linkedZoneIds.isEmpty())
|
||||||
|
{
|
||||||
|
return decideZone(zoneNames, linkedZoneIds, postcode);
|
||||||
|
}
|
||||||
List<String> tokens = PayZoneMatcher.searchTokens(product.getProductName(), product.getSheetName());
|
List<String> tokens = PayZoneMatcher.searchTokens(product.getProductName(), product.getSheetName());
|
||||||
if (tokens.isEmpty())
|
if (tokens.isEmpty())
|
||||||
{
|
{
|
||||||
@@ -148,35 +253,105 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService
|
|||||||
{
|
{
|
||||||
throw new ServiceException("产品「" + product.getProductName() + "」有多个地区,邮编分区对不上价卡上的地区");
|
throw new ServiceException("产品「" + product.getProductName() + "」有多个地区,邮编分区对不上价卡上的地区");
|
||||||
}
|
}
|
||||||
PayZoneMatcher.Decision decision = PayZoneMatcher.resolve(zoneNames, payPostcodeMapper.selectLinesByGroups(new ArrayList<>(usable)), postcode);
|
return decideZone(zoneNames, new ArrayList<>(usable), postcode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在给定邮编组里,用收件邮编决定价卡地区。对不上时抛出业务异常。
|
||||||
|
*
|
||||||
|
* @param zoneNames 价卡上的地区名
|
||||||
|
* @param groupIds 参与匹配的邮编组
|
||||||
|
* @param postcode 收件邮编
|
||||||
|
* @return 命中的地区名
|
||||||
|
*/
|
||||||
|
private String decideZone(List<String> zoneNames, List<Long> groupIds, String postcode)
|
||||||
|
{
|
||||||
|
PayZoneMatcher.Decision decision = PayZoneMatcher.resolve(zoneNames, payPostcodeMapper.selectLinesByGroups(groupIds), postcode);
|
||||||
if (decision.getMessage() != null)
|
if (decision.getMessage() != null)
|
||||||
{
|
{
|
||||||
throw new ServiceException(decision.getMessage());
|
throw new ServiceException(decision.getMessage());
|
||||||
}
|
}
|
||||||
if (decision.getZoneName() == null)
|
if (decision.getZoneName() == null)
|
||||||
{
|
{
|
||||||
throw new ServiceException("邮编没有对上产品「" + product.getProductName() + "」的地区");
|
throw new ServiceException("邮编没有对上价卡地区");
|
||||||
}
|
}
|
||||||
return decision.getZoneName();
|
return decision.getZoneName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取出用来选派送费的普通分区。偏远、附加费邮编和不可达不参与选区。
|
||||||
|
*
|
||||||
|
* @param linked 已对上的邮编组
|
||||||
|
* @param countryCode 目的国二字码
|
||||||
|
* @param countryName 目的国名称
|
||||||
|
* @return 可参与选区的邮编组ID
|
||||||
|
*/
|
||||||
|
private static List<Long> zoneGroupIds(List<PayPostcodeGroup> linked, String countryCode, String countryName)
|
||||||
|
{
|
||||||
|
List<Long> ids = new ArrayList<>();
|
||||||
|
if (linked == null)
|
||||||
|
{
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
for (PayPostcodeGroup group : linked)
|
||||||
|
{
|
||||||
|
if (group == null || group.getGroupId() == null || PayRemoteBook.remoteGroup(group) || "FEE".equals(group.getConfigKind()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ("UNREACH".equalsIgnoreCase(group.getCoverage() == null ? "" : group.getCoverage().trim()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!sameCountry(group, countryCode, countryName))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ids.add(group.getGroupId());
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收集价卡重量段和分区附加费上的地区名。
|
||||||
|
*
|
||||||
|
* @param product 当前价卡版本
|
||||||
|
* @return 去重后的地区名
|
||||||
|
*/
|
||||||
private static List<String> zoneNames(PayProduct product)
|
private static List<String> zoneNames(PayProduct product)
|
||||||
{
|
{
|
||||||
Set<String> names = new LinkedHashSet<>();
|
Set<String> names = new LinkedHashSet<>();
|
||||||
if (product.getWeightBands() == null)
|
if (product.getWeightBands() != null)
|
||||||
{
|
{
|
||||||
return new ArrayList<>();
|
for (PayWeightBand band : product.getWeightBands())
|
||||||
}
|
|
||||||
for (PayWeightBand band : product.getWeightBands())
|
|
||||||
{
|
|
||||||
if (band != null && band.getZoneName() != null && !band.getZoneName().isBlank())
|
|
||||||
{
|
{
|
||||||
names.add(band.getZoneName().trim());
|
if (band != null && band.getZoneName() != null && !band.getZoneName().isBlank())
|
||||||
|
{
|
||||||
|
names.add(band.getZoneName().trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (product.getSurcharges() != null)
|
||||||
|
{
|
||||||
|
for (PaySurcharge fee : product.getSurcharges())
|
||||||
|
{
|
||||||
|
if (fee != null && PaySurchargeRules.ZONE.equals(fee.getCalcRule()) && fee.getZoneName() != null && !fee.getZoneName().isBlank())
|
||||||
|
{
|
||||||
|
names.add(fee.getZoneName().trim());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new ArrayList<>(names);
|
return new ArrayList<>(names);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断邮编组是否和订单目的国相同。两边都没写国家时视为相同。
|
||||||
|
*
|
||||||
|
* @param group 邮编组
|
||||||
|
* @param countryCode 订单目的国二字码
|
||||||
|
* @param countryName 订单目的国名称
|
||||||
|
* @return 国家能对上时为 true
|
||||||
|
*/
|
||||||
private static boolean sameCountry(PayPostcodeGroup group, String countryCode, String countryName)
|
private static boolean sameCountry(PayPostcodeGroup group, String countryCode, String countryName)
|
||||||
{
|
{
|
||||||
if (blank(countryCode) && blank(countryName))
|
if (blank(countryCode) && blank(countryName))
|
||||||
@@ -194,6 +369,12 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService
|
|||||||
return !blank(countryName) && countryName.equals(group.getCountryName());
|
return !blank(countryName) && countryName.equals(group.getCountryName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从订单扩展找出渠道。先按渠道ID,再按渠道编码,最后按渠道名称。
|
||||||
|
*
|
||||||
|
* @param ext 订单计费扩展
|
||||||
|
* @return 服务渠道
|
||||||
|
*/
|
||||||
private ServiceChannel resolveChannel(PrcOrderBillExt ext)
|
private ServiceChannel resolveChannel(PrcOrderBillExt ext)
|
||||||
{
|
{
|
||||||
if (ext == null)
|
if (ext == null)
|
||||||
@@ -221,6 +402,14 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService
|
|||||||
return channel;
|
return channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存订单应付主表。已有记录则更新合计,没有则新建。合计只写本次尾程结果。
|
||||||
|
*
|
||||||
|
* @param bill 计费订单
|
||||||
|
* @param result 本次尾程计算结果
|
||||||
|
* @param operator 操作人账号
|
||||||
|
* @return 保存后的订单应付
|
||||||
|
*/
|
||||||
private PrcPayBill saveBill(PrcOrderBill bill, PayOrderCostResult result, String operator)
|
private PrcPayBill saveBill(PrcOrderBill bill, PayOrderCostResult result, String operator)
|
||||||
{
|
{
|
||||||
PrcPayBill existed = payBillMapper.selectByBillId(bill.getBillId());
|
PrcPayBill existed = payBillMapper.selectByBillId(bill.getBillId());
|
||||||
@@ -250,6 +439,13 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService
|
|||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用本次算出的尾程费用替换订单上的计算费用。来源不是计算的费用行会保留。
|
||||||
|
*
|
||||||
|
* @param billId 计费订单ID
|
||||||
|
* @param payBillId 订单应付主表ID
|
||||||
|
* @param fees 本次算出的费用
|
||||||
|
*/
|
||||||
private void saveFees(String billId, String payBillId, List<PayCostLine> fees)
|
private void saveFees(String billId, String payBillId, List<PayCostLine> fees)
|
||||||
{
|
{
|
||||||
payBillFeeMapper.deleteByBillId(billId);
|
payBillFeeMapper.deleteByBillId(billId);
|
||||||
@@ -281,6 +477,12 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把日期格式化成 yyyy-MM-dd,供结果说明使用。空日期表示不限。
|
||||||
|
*
|
||||||
|
* @param date 有效期起或止
|
||||||
|
* @return 日期文本
|
||||||
|
*/
|
||||||
private static String day(java.util.Date date)
|
private static String day(java.util.Date date)
|
||||||
{
|
{
|
||||||
if (date == null)
|
if (date == null)
|
||||||
@@ -294,6 +496,12 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService
|
|||||||
return date.toInstant().atZone(java.time.ZoneId.of("GMT+8")).toLocalDate().toString();
|
return date.toInstant().atZone(java.time.ZoneId.of("GMT+8")).toLocalDate().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按传入顺序取第一个大于 0 的重量。
|
||||||
|
*
|
||||||
|
* @param values 计费重、重量、实重、收货重
|
||||||
|
* @return 第一个正数;都没有时返回 null
|
||||||
|
*/
|
||||||
private static BigDecimal firstPositive(BigDecimal... values)
|
private static BigDecimal firstPositive(BigDecimal... values)
|
||||||
{
|
{
|
||||||
if (values == null)
|
if (values == null)
|
||||||
@@ -310,6 +518,12 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断文本是否为空。
|
||||||
|
*
|
||||||
|
* @param value 待判断文本
|
||||||
|
* @return 为 null 或空白时返回 true
|
||||||
|
*/
|
||||||
private static boolean blank(String value)
|
private static boolean blank(String value)
|
||||||
{
|
{
|
||||||
return value == null || value.isBlank();
|
return value == null || value.isBlank();
|
||||||
|
|||||||
@@ -100,6 +100,20 @@
|
|||||||
select group_id from pay_postcode_group where file_md5 = #{fileMd5} and sheet_name = #{sheetName}
|
select group_id from pay_postcode_group where file_md5 = #{fileMd5} and sheet_name = #{sheetName}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectGroupsForProduct" resultType="com.bms.price.domain.PayPostcodeGroup">
|
||||||
|
select group_id as groupId, block_title as blockTitle, channel_name as channelName,
|
||||||
|
product_name as productName, quote_sheet as quoteSheet, sheet_name as sheetName,
|
||||||
|
config_kind as configKind, coverage, fee_name as feeName, fee_text as feeText,
|
||||||
|
country_name as countryName, country_code as countryCode
|
||||||
|
from pay_postcode_group
|
||||||
|
where
|
||||||
|
<foreach collection="names" item="name" separator=" or ">
|
||||||
|
lower(quote_sheet) = lower(#{name})
|
||||||
|
or lower(product_name) = lower(#{name})
|
||||||
|
or lower(sheet_name) = lower(#{name})
|
||||||
|
</foreach>
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="selectGroupsByTokens" resultType="com.bms.price.domain.PayPostcodeGroup">
|
<select id="selectGroupsByTokens" resultType="com.bms.price.domain.PayPostcodeGroup">
|
||||||
select group_id as groupId, block_title as blockTitle, channel_name as channelName,
|
select group_id as groupId, block_title as blockTitle, channel_name as channelName,
|
||||||
product_name as productName, quote_sheet as quoteSheet, sheet_name as sheetName,
|
product_name as productName, quote_sheet as quoteSheet, sheet_name as sheetName,
|
||||||
|
|||||||
@@ -250,12 +250,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<insert id="insertSurcharges">
|
<insert id="insertSurcharges">
|
||||||
insert into pay_surcharge (
|
insert into pay_surcharge (
|
||||||
product_id, version_no, section_name, fee_name, zone_name, charge_text, amount, currency, price_unit, fee_desc, period_text,
|
product_id, version_no, section_name, fee_name, zone_name, charge_text, amount, currency, price_unit, fee_desc, period_text,
|
||||||
effective_start, effective_end, charge_timing, calc_rule, calc_param, sort_no
|
effective_start, effective_end, charge_timing, calc_rule, calc_param, weight_min, weight_max, postcode_param, sort_no
|
||||||
) values
|
) values
|
||||||
<foreach collection="list" item="row" separator=",">
|
<foreach collection="list" item="row" separator=",">
|
||||||
(#{row.productId}, ifnull(#{row.versionNo}, 1), #{row.sectionName}, #{row.feeName}, #{row.zoneName}, #{row.chargeText}, #{row.amount},
|
(#{row.productId}, ifnull(#{row.versionNo}, 1), #{row.sectionName}, #{row.feeName}, #{row.zoneName}, #{row.chargeText}, #{row.amount},
|
||||||
#{row.currency}, #{row.priceUnit}, #{row.feeDesc}, #{row.periodText}, #{row.effectiveStart}, #{row.effectiveEnd},
|
#{row.currency}, #{row.priceUnit}, #{row.feeDesc}, #{row.periodText}, #{row.effectiveStart}, #{row.effectiveEnd},
|
||||||
#{row.chargeTiming}, ifnull(#{row.calcRule}, 'MANUAL'), #{row.calcParam}, #{row.sortNo})
|
#{row.chargeTiming}, ifnull(#{row.calcRule}, 'MANUAL'), #{row.calcParam}, #{row.weightMin}, #{row.weightMax},
|
||||||
|
#{row.postcodeParam}, #{row.sortNo})
|
||||||
</foreach>
|
</foreach>
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
@@ -280,7 +281,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
select surcharge_id as surchargeId, product_id as productId, version_no as versionNo, section_name as sectionName, fee_name as feeName,
|
select surcharge_id as surchargeId, product_id as productId, version_no as versionNo, section_name as sectionName, fee_name as feeName,
|
||||||
zone_name as zoneName, charge_text as chargeText, amount, currency, price_unit as priceUnit, fee_desc as feeDesc,
|
zone_name as zoneName, charge_text as chargeText, amount, currency, price_unit as priceUnit, fee_desc as feeDesc,
|
||||||
period_text as periodText, effective_start as effectiveStart, effective_end as effectiveEnd,
|
period_text as periodText, effective_start as effectiveStart, effective_end as effectiveEnd,
|
||||||
charge_timing as chargeTiming, calc_rule as calcRule, calc_param as calcParam, sort_no as sortNo
|
charge_timing as chargeTiming, calc_rule as calcRule, calc_param as calcParam,
|
||||||
|
weight_min as weightMin, weight_max as weightMax, postcode_param as postcodeParam, sort_no as sortNo
|
||||||
from pay_surcharge
|
from pay_surcharge
|
||||||
where product_id in
|
where product_id in
|
||||||
<foreach collection="productIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
<foreach collection="productIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
-- 偏远附加费按重量区间和邮编命中。已有库可重复执行。
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
SET @sql = (
|
||||||
|
SELECT IF(
|
||||||
|
COUNT(*) = 0,
|
||||||
|
'ALTER TABLE pay_surcharge ADD COLUMN weight_min decimal(12,3) DEFAULT NULL COMMENT ''适用重量下限KG,空表示不限'' AFTER calc_param',
|
||||||
|
'SELECT 1'
|
||||||
|
)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pay_surcharge' AND COLUMN_NAME = 'weight_min'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql = (
|
||||||
|
SELECT IF(
|
||||||
|
COUNT(*) = 0,
|
||||||
|
'ALTER TABLE pay_surcharge ADD COLUMN weight_max decimal(12,3) DEFAULT NULL COMMENT ''适用重量上限KG,空表示不限'' AFTER weight_min',
|
||||||
|
'SELECT 1'
|
||||||
|
)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pay_surcharge' AND COLUMN_NAME = 'weight_max'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql = (
|
||||||
|
SELECT IF(
|
||||||
|
COUNT(*) = 0,
|
||||||
|
'ALTER TABLE pay_surcharge ADD COLUMN postcode_param varchar(1000) DEFAULT NULL COMMENT ''偏远邮编规则,如07,35或IV,KA27-28'' AFTER weight_max',
|
||||||
|
'SELECT 1'
|
||||||
|
)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pay_surcharge' AND COLUMN_NAME = 'postcode_param'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@@ -117,8 +117,11 @@
|
|||||||
<span v-else>{{ calcRuleLabel(row.calcRule) }}</span>
|
<span v-else>{{ calcRuleLabel(row.calcRule) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="calcParam" label="条件参数" min-width="180" show-overflow-tooltip>
|
<el-table-column prop="calcParam" label="条件参数" min-width="220" show-overflow-tooltip>
|
||||||
<template #default="{ row }"><cell-text v-model="row.calcParam" :edit="editable" /></template>
|
<template #default="{ row }">
|
||||||
|
<div>{{ feeCondition(row) }}</div>
|
||||||
|
<cell-text v-if="editable" v-model="row.calcParam" :edit="editable" />
|
||||||
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="editable" label="操作" width="70" align="center">
|
<el-table-column v-if="editable" label="操作" width="70" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
@@ -195,6 +198,8 @@ const CellText = {
|
|||||||
const calcRules = [
|
const calcRules = [
|
||||||
{ value: 'ALWAYS', label: '每票都收' },
|
{ value: 'ALWAYS', label: '每票都收' },
|
||||||
{ value: 'POSTCODE', label: '命中邮编' },
|
{ value: 'POSTCODE', label: '命中邮编' },
|
||||||
|
{ value: 'ZONE', label: '命中分区' },
|
||||||
|
{ value: 'EXTRA_KG', label: '超重续收' },
|
||||||
{ value: 'WEIGHT', label: '超重' },
|
{ value: 'WEIGHT', label: '超重' },
|
||||||
{ value: 'SIZE', label: '超尺寸' },
|
{ value: 'SIZE', label: '超尺寸' },
|
||||||
{ value: 'RETURN', label: '退件' },
|
{ value: 'RETURN', label: '退件' },
|
||||||
@@ -205,6 +210,22 @@ function calcRuleLabel(value) {
|
|||||||
return calcRules.find(item => item.value === value)?.label || '人工确认'
|
return calcRules.find(item => item.value === value)?.label || '人工确认'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function feeCondition(row) {
|
||||||
|
const parts = []
|
||||||
|
const hasMin = row.weightMin != null && row.weightMin !== ''
|
||||||
|
const hasMax = row.weightMax != null && row.weightMax !== ''
|
||||||
|
if (hasMin || hasMax) {
|
||||||
|
parts.push(`${hasMin ? row.weightMin : ''}-${hasMax ? row.weightMax : ''}kg`)
|
||||||
|
}
|
||||||
|
if (row.postcodeParam) {
|
||||||
|
parts.push(`邮编 ${row.postcodeParam}`)
|
||||||
|
}
|
||||||
|
if (row.calcParam && row.calcParam !== row.postcodeParam) {
|
||||||
|
parts.push(String(row.calcParam))
|
||||||
|
}
|
||||||
|
return parts.join(';') || '-'
|
||||||
|
}
|
||||||
|
|
||||||
const pageSize = 20
|
const pageSize = 20
|
||||||
const panes = ref([])
|
const panes = ref([])
|
||||||
|
|
||||||
@@ -229,7 +250,7 @@ function filteredBands(pane) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function filteredFees(pane) {
|
function filteredFees(pane) {
|
||||||
return filterRows(pane.item.surcharges || [], pane.feeKeyword, ['feeName', 'zoneName', 'chargeText', 'amount', 'currency', 'priceUnit', 'feeDesc', 'calcRule', 'calcParam'])
|
return filterRows(pane.item.surcharges || [], pane.feeKeyword, ['feeName', 'zoneName', 'chargeText', 'amount', 'currency', 'priceUnit', 'feeDesc', 'calcRule', 'calcParam', 'postcodeParam'])
|
||||||
}
|
}
|
||||||
|
|
||||||
function addBand(pane) {
|
function addBand(pane) {
|
||||||
|
|||||||
@@ -182,7 +182,6 @@
|
|||||||
<template #col-action="{ row }">
|
<template #col-action="{ row }">
|
||||||
<el-button link type="primary" icon="View" @click="handleView(row)">查看</el-button>
|
<el-button link type="primary" icon="View" @click="handleView(row)">查看</el-button>
|
||||||
<el-button link type="primary" icon="Money" v-hasPermi="['price:billing:bill']" @click="handlePayBill(row)">生成应付</el-button>
|
<el-button link type="primary" icon="Money" v-hasPermi="['price:billing:bill']" @click="handlePayBill(row)">生成应付</el-button>
|
||||||
<el-button
|
|
||||||
<!-- <el-button
|
<!-- <el-button
|
||||||
link
|
link
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -579,8 +578,7 @@ const columns = [
|
|||||||
{ label: '销售员', prop: 'belongSaler', minWidth: 100, showOverflowTooltip: true },
|
{ label: '销售员', prop: 'belongSaler', minWidth: 100, showOverflowTooltip: true },
|
||||||
{ label: '结算员', prop: 'settlementMan', minWidth: 100, showOverflowTooltip: true },
|
{ label: '结算员', prop: 'settlementMan', minWidth: 100, showOverflowTooltip: true },
|
||||||
{ label: '客服员', prop: 'serviceMan', minWidth: 100, showOverflowTooltip: true },
|
{ label: '客服员', prop: 'serviceMan', minWidth: 100, showOverflowTooltip: true },
|
||||||
{ label: '操作', prop: 'action', align: 'center', width: 320, fixed: 'right', slot: 'action', showInToolbar: false, showOverflowTooltip: false }
|
{ label: '操作', prop: 'action', align: 'center', width: 260, fixed: 'right', slot: 'action', showInToolbar: false, showOverflowTooltip: false }
|
||||||
{ label: '操作', prop: 'action', align: 'center', width: 150, fixed: 'right', slot: 'action', showInToolbar: false, showOverflowTooltip: false }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const arrearsStatusOptions = [
|
const arrearsStatusOptions = [
|
||||||
|
|||||||
@@ -212,7 +212,6 @@ import { deleteIncomeBillFee, getBill } from '@/api/price/billing'
|
|||||||
import { generatePayBill } from '@/api/price/pay'
|
import { generatePayBill } from '@/api/price/pay'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import BillBillingHistoryDrawer from './BillBillingHistoryDrawer.vue'
|
import BillBillingHistoryDrawer from './BillBillingHistoryDrawer.vue'
|
||||||
import { deleteIncomeBillFee } from '@/api/price/billing'
|
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user