feat(fleet): read attributable trip receivables and receipts from finance
This commit is contained in:
+31
@@ -0,0 +1,31 @@
|
||||
package com.baiyue.finance.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.finance.dto.TripFinanceModels;
|
||||
import com.baiyue.finance.service.TripFinanceQueryService;
|
||||
import com.baiyue.finance.service.FinanceEventException;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/internal/finance/v1/trip-receivables")
|
||||
public class InternalTripFinanceController {
|
||||
private final TripFinanceQueryService service;
|
||||
private final String expectedToken;
|
||||
public InternalTripFinanceController(TripFinanceQueryService service,
|
||||
@Value("${baiyue.security.internal-service-token}") String expectedToken) {
|
||||
this.service=service;this.expectedToken=expectedToken;
|
||||
}
|
||||
@PostMapping("/query")
|
||||
public ApiResponse<TripFinanceModels.Result> query(
|
||||
@RequestHeader(value="X-Internal-Service-Token",required=false) String token,
|
||||
@Valid @RequestBody TripFinanceModels.Query query) {
|
||||
if(token==null || expectedToken==null || expectedToken.isBlank()
|
||||
|| !MessageDigest.isEqual(token.getBytes(StandardCharsets.UTF_8),expectedToken.getBytes(StandardCharsets.UTF_8)))
|
||||
throw new FinanceEventException("FIN-EVT-401","内部服务令牌无效");
|
||||
return FinanceResponses.success(service.query(query));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.baiyue.finance.dto;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
public final class TripFinanceModels {
|
||||
private TripFinanceModels() { }
|
||||
public record Business(@Positive long customerId, @NotBlank @Size(max=64) String deliveryNo) { }
|
||||
public record Query(@NotNull @Size(max=200) List<@Valid Business> businesses, boolean receiptsVisible) { }
|
||||
public record Money(String currency, BigDecimal receivableAmount, BigDecimal receivedAmount) { }
|
||||
public record Source(long customerId, String businessNo, String sourceNo, String feeItemName, int sourceVersion,
|
||||
String currency, BigDecimal amount, String status) { }
|
||||
public record Document(String billNo, String currency, BigDecimal receivedAmount, String allocationStatus, String reason) { }
|
||||
public record Result(String status, List<Money> totals, List<Source> sources, List<Document> documents, String reason) { }
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.baiyue.finance.service;
|
||||
|
||||
import com.baiyue.finance.dto.TripFinanceModels;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
|
||||
/** Only whole documents whose complete source set belongs to this trip are attributable. No ratios. */
|
||||
@Service
|
||||
public class TripFinanceQueryService {
|
||||
private final JdbcTemplate jdbc;
|
||||
public TripFinanceQueryService(JdbcTemplate jdbc) {this.jdbc=jdbc;}
|
||||
record Fee(long id,long customerId,String businessNo,String sourceNo,String name,int version,String currency,BigDecimal amount,String status) { }
|
||||
record Bill(long id,long customerId,String no,String currency) { }
|
||||
@Transactional(readOnly=true, isolation=org.springframework.transaction.annotation.Isolation.REPEATABLE_READ)
|
||||
public TripFinanceModels.Result query(TripFinanceModels.Query request) {
|
||||
if(request.businesses().isEmpty())return new TripFinanceModels.Result("PENDING_ALLOCATION",List.of(),List.of(),List.of(),"尚无可明确归属车次的派送费用来源");
|
||||
var args=new HashMap<String,Object>();var predicates=new ArrayList<String>();
|
||||
int index=0;
|
||||
for(var b:request.businesses().stream().distinct().toList()) {
|
||||
predicates.add("(f.customer_id=:customer"+index+" AND f.business_no=:business"+index+")");
|
||||
args.put("customer"+index,b.customerId());args.put("business"+index,b.deliveryNo());index++;
|
||||
}
|
||||
var fees=new NamedParameterJdbcTemplate(jdbc).query("""
|
||||
SELECT f.id,f.customer_id,f.business_no,f.source_no,f.fee_item_name,f.source_fee_version,f.currency_code,f.amount,f.status
|
||||
FROM fin_fee_source f WHERE f.source_system='LASTLEG' AND f.business_type='DELIVERY_ORDER'
|
||||
AND f.source_type='OVERSEAS_DELIVERY_FEE' AND f.direction='RECEIVABLE'
|
||||
AND f.review_status='APPROVED' AND f.status IN ('AVAILABLE','SETTLED')
|
||||
AND NOT EXISTS (SELECT 1 FROM fin_fee_source newer WHERE newer.source_system=f.source_system
|
||||
AND newer.source_type=f.source_type AND newer.source_fee_id=f.source_fee_id
|
||||
AND newer.source_fee_version>f.source_fee_version)
|
||||
AND (
|
||||
"""+String.join(" OR ",predicates)+") ORDER BY f.customer_id,f.business_no,f.source_fee_id LIMIT 2001",
|
||||
args,(r,i)->new Fee(r.getLong("id"),r.getLong("customer_id"),r.getString("business_no"),r.getString("source_no"),r.getString("fee_item_name"),r.getInt("source_fee_version"),r.getString("currency_code"),r.getBigDecimal("amount"),r.getString("status")));
|
||||
if(fees.size()>2000)throw new FinanceEventException("FIN_400","车次费用来源超过2000条,请核对来源范围");
|
||||
return project(fees,request);
|
||||
}
|
||||
TripFinanceModels.Result project(List<Fee> fees,TripFinanceModels.Query request) {
|
||||
var receivables=new TreeMap<String,BigDecimal>();var received=new TreeMap<String,BigDecimal>();
|
||||
var pending=new HashSet<String>();var docs=new ArrayList<TripFinanceModels.Document>();
|
||||
var eligible=new HashMap<Long,Fee>();fees.forEach(f->{eligible.put(f.id(),f);receivables.merge(f.currency(),f.amount(),BigDecimal::add);});
|
||||
boolean missing=request.businesses().stream().anyMatch(b->fees.stream().noneMatch(f->f.customerId()==b.customerId()&&f.businessNo().equals(b.deliveryNo())));
|
||||
if(request.receiptsVisible()&&!fees.isEmpty()) {
|
||||
var named=new NamedParameterJdbcTemplate(jdbc);
|
||||
var refs=named.query("""
|
||||
SELECT b.id,b.tenant_id,b.bill_no,b.currency_code,l.source_id FROM fin_bill_line l
|
||||
JOIN fin_bill b ON b.id=l.bill_id AND b.tenant_id=l.tenant_id
|
||||
JOIN fin_fee_source f ON f.id=l.source_id AND f.customer_id=l.tenant_id AND f.source_fee_version=l.source_version
|
||||
WHERE l.source_id IN (:ids) AND b.status='ISSUED'
|
||||
""",Map.of("ids",eligible.keySet()),(r,i)->new Object[]{
|
||||
new Bill(r.getLong("id"),r.getLong("tenant_id"),r.getString("bill_no"),r.getString("currency_code")),r.getLong("source_id")});
|
||||
var bills=new LinkedHashMap<Long,Bill>();var byFee=new HashMap<Long,Set<Long>>();
|
||||
for(var ref:refs){var bill=(Bill)ref[0];bills.put(bill.id(),bill);byFee.computeIfAbsent((Long)ref[1],k->new HashSet<>()).add(bill.id());}
|
||||
for(var fee:fees)if(byFee.getOrDefault(fee.id(),Set.of()).size()!=1)pending.add(fee.currency());
|
||||
if(!bills.isEmpty()) {
|
||||
var lines=named.query("SELECT bill_id,tenant_id,source_id,source_version,currency_code FROM fin_bill_line WHERE bill_id IN (:ids)",
|
||||
Map.of("ids",bills.keySet()),(r,i)->new Object[]{r.getLong("bill_id"),r.getLong("tenant_id"),r.getLong("source_id"),r.getInt("source_version"),r.getString("currency_code")});
|
||||
var byBill=new HashMap<Long,List<Object[]>>();for(var line:lines)byBill.computeIfAbsent((Long)line[0],k->new ArrayList<>()).add(line);
|
||||
var payments=named.query("""
|
||||
SELECT rb.bill_id,r.currency_code,SUM(rb.applied_amount) amount FROM fin_receipt_bill rb
|
||||
JOIN fin_receipt r ON r.id=rb.receipt_id AND r.tenant_id=rb.tenant_id
|
||||
JOIN fin_bill b ON b.id=rb.bill_id AND b.tenant_id=rb.tenant_id AND b.currency_code=r.currency_code
|
||||
WHERE rb.bill_id IN (:ids) AND r.status='CONFIRMED' GROUP BY rb.bill_id,r.currency_code
|
||||
""",Map.of("ids",bills.keySet()),(r,i)->new Object[]{r.getLong("bill_id"),r.getString("currency_code"),r.getBigDecimal("amount")});
|
||||
var paidByBill=new HashMap<Long,BigDecimal>();for(var payment:payments)paidByBill.put((Long)payment[0],(BigDecimal)payment[2]);
|
||||
for(var bill:bills.values()) {
|
||||
var billLines=byBill.getOrDefault(bill.id(),List.of());
|
||||
boolean exact=!billLines.isEmpty()&&billLines.stream().allMatch(l->{var f=eligible.get((Long)l[2]);return f!=null&&f.customerId()==bill.customerId()&&(Long)l[1]==bill.customerId()&&f.version()==(Integer)l[3]&&f.currency().equals(bill.currency())&&f.currency().equals(l[4]);});
|
||||
BigDecimal paid=exact?paidByBill.get(bill.id()):null;
|
||||
String reason=!exact?"账单包含本车次以外或无法明确归属的来源,实收待分配":paid==null?"尚无已确认的本车次收款分配":"整张账单的全部来源明确归属本车次";
|
||||
if(paid==null)pending.add(bill.currency());else received.merge(bill.currency(),paid,BigDecimal::add);
|
||||
docs.add(new TripFinanceModels.Document(bill.no(),bill.currency(),paid,paid==null?"PENDING_ALLOCATION":"ATTRIBUTED",reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
var totals=receivables.entrySet().stream().map(e->new TripFinanceModels.Money(e.getKey(),e.getValue(),
|
||||
!request.receiptsVisible()||pending.contains(e.getKey())?null:received.get(e.getKey()))).toList();
|
||||
String status=fees.isEmpty()||missing?"PENDING_ALLOCATION":"AVAILABLE";
|
||||
String reason=missing?"部分派送尚无已审核有效的Finance来源,已确认金额不代表完整车次应收":"仅包含已审核有效的Finance来源;实收只计明确归属本车次的已确认分配";
|
||||
return new TripFinanceModels.Result(status,totals,fees.stream().map(f->new TripFinanceModels.Source(f.customerId(),f.businessNo(),f.sourceNo(),f.name(),f.version(),f.currency(),f.amount(),f.status())).toList(),List.copyOf(docs),reason);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "车次Finance权威应收与实收只读来源",
|
||||
"version": "1.0.0",
|
||||
"description": "Lastleg以不可变DEPART验证后的客户/派送业务号读取。仅本Finance库;最新已审核有效LASTLEG OVERSEAS_DELIVERY_FEE应收,按币种。实收只取fin_receipt_bill已确认分配;整张ISSUED账单全部来源均明确归属请求车次时才计一次。混合账单、无分配、无有效来源为待分配,不按账单总额或比例估算。最多200业务、2000费用,批量查询。现行应收结算单收款沿关联账单,fin_settlement_payment仅用于应付,不重复叠加。"
|
||||
},
|
||||
"paths": {
|
||||
"/internal/finance/v1/trip-receivables/query": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"internalToken": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"businesses",
|
||||
"receiptsVisible"
|
||||
],
|
||||
"properties": {
|
||||
"businesses": {
|
||||
"type": "array",
|
||||
"maxItems": 200,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"customerId",
|
||||
"deliveryNo"
|
||||
],
|
||||
"properties": {
|
||||
"customerId": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 1
|
||||
},
|
||||
"deliveryNo": {
|
||||
"type": "string",
|
||||
"maxLength": 64
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"receiptsVisible": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Finance来源及归属,不生成费用/账单/分配",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/TripFinanceAuthority"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "输入或来源数量超限"
|
||||
},
|
||||
"401": {
|
||||
"description": "内部令牌无效"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"internalToken": {
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-Internal-Service-Token"
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"TripFinanceMoney": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"currency",
|
||||
"receivableAmount",
|
||||
"receivedAmount"
|
||||
],
|
||||
"properties": {
|
||||
"currency": {
|
||||
"type": "string"
|
||||
},
|
||||
"receivableAmount": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"receivedAmount": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"TripFinanceSource": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"customerId",
|
||||
"businessNo",
|
||||
"sourceNo",
|
||||
"feeItemName",
|
||||
"sourceVersion",
|
||||
"currency",
|
||||
"amount",
|
||||
"status"
|
||||
],
|
||||
"properties": {
|
||||
"customerId": {
|
||||
"type": "string"
|
||||
},
|
||||
"businessNo": {
|
||||
"type": "string"
|
||||
},
|
||||
"sourceNo": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"feeItemName": {
|
||||
"type": "string"
|
||||
},
|
||||
"sourceVersion": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"currency": {
|
||||
"type": "string"
|
||||
},
|
||||
"amount": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TripFinanceDocument": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"billNo",
|
||||
"currency",
|
||||
"receivedAmount",
|
||||
"allocationStatus",
|
||||
"reason"
|
||||
],
|
||||
"properties": {
|
||||
"billNo": {
|
||||
"type": "string"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string"
|
||||
},
|
||||
"receivedAmount": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"nullable": true
|
||||
},
|
||||
"allocationStatus": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ATTRIBUTED",
|
||||
"PENDING_ALLOCATION"
|
||||
]
|
||||
},
|
||||
"reason": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TripFinanceAuthority": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"status",
|
||||
"totals",
|
||||
"sources",
|
||||
"documents",
|
||||
"reason"
|
||||
],
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"AVAILABLE",
|
||||
"PENDING_ALLOCATION"
|
||||
]
|
||||
},
|
||||
"totals": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TripFinanceMoney"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TripFinanceSource"
|
||||
}
|
||||
},
|
||||
"documents": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TripFinanceDocument"
|
||||
}
|
||||
},
|
||||
"reason": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.baiyue.finance.controller;
|
||||
import com.baiyue.finance.dto.TripFinanceModels;
|
||||
import com.baiyue.finance.service.TripFinanceQueryService;
|
||||
import com.baiyue.finance.service.FinanceEventException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
class InternalTripFinanceControllerTest {
|
||||
@Test void rejectsAbsentOrWrongInternalTokenBeforeReadingFinance() {
|
||||
var service=mock(TripFinanceQueryService.class);var controller=new InternalTripFinanceController(service,"synthetic-token");
|
||||
var query=new TripFinanceModels.Query(List.of(),false);
|
||||
assertThatThrownBy(()->controller.query(null,query)).isInstanceOf(FinanceEventException.class);
|
||||
assertThatThrownBy(()->controller.query("wrong",query)).isInstanceOf(FinanceEventException.class);
|
||||
verifyNoInteractions(service);
|
||||
}
|
||||
@Test void validInternalTokenReturnsOnlyTheProviderResult() {
|
||||
var service=mock(TripFinanceQueryService.class);var query=new TripFinanceModels.Query(List.of(),false);
|
||||
var result=new TripFinanceModels.Result("PENDING_ALLOCATION",List.of(),List.of(),List.of(),"pending");
|
||||
when(service.query(query)).thenReturn(result);
|
||||
assertThat(new InternalTripFinanceController(service,"synthetic-token").query("synthetic-token",query).data()).isSameAs(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.baiyue.finance.service;
|
||||
import com.baiyue.finance.dto.TripFinanceModels;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import java.util.*;
|
||||
import java.math.BigDecimal;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
class TripFinanceQueryServiceTest {
|
||||
JdbcTemplate jdbc;TripFinanceQueryService service;
|
||||
@BeforeEach void setup() {
|
||||
jdbc=new JdbcTemplate(new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1","sa",""));
|
||||
jdbc.execute("CREATE TABLE fin_fee_source(id BIGINT,customer_id BIGINT,business_no VARCHAR(64),business_type VARCHAR(32),source_no VARCHAR(64),fee_item_name VARCHAR(64),source_fee_id BIGINT,source_fee_version INT,source_system VARCHAR(32),source_type VARCHAR(32),direction VARCHAR(32),review_status VARCHAR(32),status VARCHAR(32),currency_code VARCHAR(3),amount DECIMAL(20,6))");
|
||||
jdbc.execute("CREATE TABLE fin_bill(id BIGINT,tenant_id BIGINT,bill_no VARCHAR(64),currency_code VARCHAR(3),status VARCHAR(16))");
|
||||
jdbc.execute("CREATE TABLE fin_bill_line(tenant_id BIGINT,bill_id BIGINT,source_id BIGINT,source_version INT,currency_code VARCHAR(3))");
|
||||
jdbc.execute("CREATE TABLE fin_receipt(id BIGINT,tenant_id BIGINT,status VARCHAR(16),currency_code VARCHAR(3))");
|
||||
jdbc.execute("CREATE TABLE fin_receipt_bill(tenant_id BIGINT,receipt_id BIGINT,bill_id BIGINT,applied_amount DECIMAL(20,6))");
|
||||
service=new TripFinanceQueryService(jdbc);
|
||||
}
|
||||
void fee(long id,long customer,String business,int version,String currency,String amount,String status) {
|
||||
jdbc.update("INSERT INTO fin_fee_source VALUES(?,?,?,'DELIVERY_ORDER',?,'Delivery',99,?,'LASTLEG','OVERSEAS_DELIVERY_FEE','RECEIVABLE','APPROVED',?,?,?)",id,customer,business,business,version,status,currency,new BigDecimal(amount));
|
||||
}
|
||||
TripFinanceModels.Query request(boolean receipts) {return new TripFinanceModels.Query(List.of(new TripFinanceModels.Business(1,"DL-A")),receipts);}
|
||||
void bill(long source,int version) {
|
||||
jdbc.update("INSERT INTO fin_bill VALUES(1,1,'BILL-A','CNY','ISSUED')");
|
||||
jdbc.update("INSERT INTO fin_bill_line VALUES(1,1,?,?,'CNY')",source,version);
|
||||
}
|
||||
void receipt(String amount) {
|
||||
jdbc.update("INSERT INTO fin_receipt VALUES(1,1,'CONFIRMED','CNY')");
|
||||
jdbc.update("INSERT INTO fin_receipt_bill VALUES(1,1,1,?)",new BigDecimal(amount));
|
||||
}
|
||||
@Test void exactWholeBillUsesRecordedAllocationAmount() {
|
||||
fee(1,1,"DL-A",1,"CNY","20","AVAILABLE");bill(1,1);receipt("3.5");
|
||||
var result=service.query(request(true));assertThat(result.totals().get(0).receivedAmount()).isEqualByComparingTo("3.5");
|
||||
assertThat(result.totals().get(0).receivableAmount()).isEqualByComparingTo("20");
|
||||
assertThat(result.documents().get(0).allocationStatus()).isEqualTo("ATTRIBUTED");
|
||||
}
|
||||
@Test void mixedBillNeverProratesReceipt() {
|
||||
fee(1,1,"DL-A",1,"CNY","20","AVAILABLE");bill(1,1);receipt("30");
|
||||
jdbc.update("INSERT INTO fin_bill_line VALUES(1,1,999,1,'CNY')");
|
||||
var result=service.query(request(true));assertThat(result.totals().get(0).receivedAmount()).isNull();
|
||||
assertThat(result.documents().get(0).billNo()).isEqualTo("BILL-A");
|
||||
assertThat(result.documents().get(0).allocationStatus()).isEqualTo("PENDING_ALLOCATION");
|
||||
}
|
||||
@Test void unbilledOrNoConfirmedReceiptIsUnknownNotZero() {
|
||||
fee(1,1,"DL-A",1,"CNY","0","AVAILABLE");
|
||||
assertThat(service.query(request(true)).totals().get(0).receivedAmount()).isNull();
|
||||
bill(1,1);assertThat(service.query(request(true)).totals().get(0).receivedAmount()).isNull();
|
||||
assertThat(service.query(request(true)).totals().get(0).receivableAmount()).isEqualByComparingTo("0");
|
||||
}
|
||||
@Test void latestVoidedDoesNotFallBackAndOtherCustomerExcluded() {
|
||||
fee(1,1,"DL-A",1,"CNY","20","AVAILABLE");fee(2,1,"DL-A",2,"CNY","20","VOIDED");
|
||||
fee(3,2,"DL-A",1,"CNY","999","AVAILABLE");
|
||||
var result=service.query(request(true));assertThat(result.sources()).isEmpty();assertThat(result.status()).isEqualTo("PENDING_ALLOCATION");
|
||||
}
|
||||
@Test void currenciesSeparateAndNoReceiptPermissionMasksDocuments() {
|
||||
fee(1,1,"DL-A",1,"CNY","20","AVAILABLE");
|
||||
jdbc.update("UPDATE fin_fee_source SET source_fee_id=1 WHERE id=1");
|
||||
fee(2,1,"DL-A",1,"USD","5","AVAILABLE");
|
||||
var result=service.query(request(false));assertThat(result.totals()).extracting(TripFinanceModels.Money::currency).containsExactly("CNY","USD");
|
||||
assertThat(result.totals()).allSatisfy(m->assertThat(m.receivedAmount()).isNull());assertThat(result.documents()).isEmpty();
|
||||
}
|
||||
@Test void wholeTripBillIsCountedOnceAcrossTwoFeeLines() {
|
||||
fee(1,1,"DL-A",1,"CNY","20","AVAILABLE");jdbc.update("UPDATE fin_fee_source SET source_fee_id=1 WHERE id=1");
|
||||
fee(2,1,"DL-A",1,"CNY","10","AVAILABLE");bill(1,1);jdbc.update("INSERT INTO fin_bill_line VALUES(1,1,2,1,'CNY')");receipt("7");
|
||||
var result=service.query(request(true));assertThat(result.documents()).hasSize(1);assertThat(result.totals().get(0).receivedAmount()).isEqualByComparingTo("7");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user