@chenchaolong/plugin-trade-compliance-workbench 1.0.12 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1480,13 +1480,11 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1480
1480
  const supplier = await this.upsertSupplier(manager, scope, draft.supplier);
1481
1481
  task.status = 'PERSISTING';
1482
1482
  await taskRepo.save(task);
1483
- const entity = await orderRepo.save(orderRepo.create({ ...auditScope(scope), supplierId: supplier.id, contractNo: draft.order.contractNo.trim(), normalizedContractNo, orderDate: draft.order.orderDate ?? null, contractAmountCny: draft.order.contractAmountCny, paidAmountCny: draft.order.paidAmountCny ?? '0', refundedAmountCny: draft.order.refundedAmountCny ?? '0', sourceFileId: task.sourceFileId, sourceFileName: task.sourceFileName, importTaskId: task.id, amountMismatchConfirmedAt: draft.order.amountMismatch === true ? confirmedAt : null, amountMismatchConfirmedById: draft.order.amountMismatch === true ? confirmedById : null }));
1483
+ const entity = await orderRepo.save(orderRepo.create({ ...auditScope(scope), supplierId: supplier.id, contractNo: draft.order.contractNo.trim(), normalizedContractNo, orderDate: optionalString(draft.order.orderDate), contractAmountCny: defaultDecimalText(draft.order.contractAmountCny), paidAmountCny: defaultDecimalText(draft.order.paidAmountCny), refundedAmountCny: defaultDecimalText(draft.order.refundedAmountCny), sourceFileId: task.sourceFileId, sourceFileName: task.sourceFileName, importTaskId: task.id, amountMismatchConfirmedAt: draft.order.amountMismatch === true ? confirmedAt : null, amountMismatchConfirmedById: draft.order.amountMismatch === true ? confirmedById : null }));
1484
1484
  const lineRepo = manager.getRepository(PurchaseOrderLine);
1485
1485
  for (let index = 0; index < draft.lines.length; index += 1) {
1486
1486
  const line = draft.lines[index];
1487
- if (new Decimal(line.quantity).lte(0))
1488
- throw new DomainError(DomainErrorCodes.INVALID_INPUT, '采购商品数量必须大于 0', { lineNumber: index + 1 });
1489
- await lineRepo.save(lineRepo.create({ ...auditScope(scope), purchaseOrderId: entity.id, lineNumber: line.lineNumber ?? index + 1, sourceType: line.sourceType ?? 'CONTRACT_EXTRACTED', productName: line.productName.trim(), normalizedProductName: normalizeForProductMatch(line.productName), englishName: line.englishName ?? null, englishNameSource: line.englishNameSource ?? null, productDescription: line.productDescription ?? null, modelSpecification: line.modelSpecification ?? null, unit: line.unit ?? null, quantity: line.quantity, unitPriceTaxIncluded: line.unitPriceTaxIncluded, lineAmountTaxIncluded: line.lineAmountTaxIncluded, vatRate: line.vatRate ?? null, vatRateSource: line.vatRateSource ?? null, hsCode: line.hsCode ?? null, hsCodeStatus: line.hsCodeStatus ?? 'UNCONFIRMED', hsCodeSource: line.hsCodeSource ?? null, hsCodeReferenceSnapshotId: line.hsCodeReferenceSnapshotId ?? null, originCountry: line.originCountry ?? null, domesticSourceLocation: line.domesticSourceLocation ?? null, refundRate: line.refundRate ?? null, refundRateSource: line.refundRateSource ?? null, refundPolicyReference: line.refundPolicyReference ?? null, refundPolicyDate: line.refundPolicyDate ?? null, refundValidationMethod: line.refundValidationMethod, refundConfirmedAt: confirmedAt, refundConfirmedById: confirmedById, amountMismatchConfirmedAt: line.amountMismatch === true ? confirmedAt : null, amountMismatchConfirmedById: line.amountMismatch === true ? confirmedById : null, sourceLocation: line.sourceLocation ?? null, sourceContent: line.sourceContent ?? null }));
1487
+ await lineRepo.save(lineRepo.create({ ...auditScope(scope), purchaseOrderId: entity.id, lineNumber: line.lineNumber ?? index + 1, sourceType: line.sourceType ?? 'CONTRACT_EXTRACTED', productName: line.productName.trim(), normalizedProductName: normalizeForProductMatch(line.productName), englishName: line.englishName ?? null, englishNameSource: line.englishNameSource ?? null, productDescription: line.productDescription ?? null, modelSpecification: line.modelSpecification ?? null, unit: line.unit ?? null, quantity: defaultDecimalText(line.quantity), unitPriceTaxIncluded: defaultDecimalText(line.unitPriceTaxIncluded), lineAmountTaxIncluded: defaultDecimalText(line.lineAmountTaxIncluded), vatRate: optionalString(line.vatRate), vatRateSource: line.vatRateSource ?? null, hsCode: line.hsCode ?? null, hsCodeStatus: line.hsCodeStatus ?? 'UNCONFIRMED', hsCodeSource: line.hsCodeSource ?? null, hsCodeReferenceSnapshotId: line.hsCodeReferenceSnapshotId ?? null, originCountry: line.originCountry ?? null, domesticSourceLocation: line.domesticSourceLocation ?? null, refundRate: optionalString(line.refundRate), refundRateSource: line.refundRateSource ?? null, refundPolicyReference: line.refundPolicyReference ?? null, refundPolicyDate: optionalString(line.refundPolicyDate), refundValidationMethod: line.refundValidationMethod ?? null, refundConfirmedAt: confirmedAt, refundConfirmedById: confirmedById, amountMismatchConfirmedAt: line.amountMismatch === true ? confirmedAt : null, amountMismatchConfirmedById: line.amountMismatch === true ? confirmedById : null, sourceLocation: line.sourceLocation ?? null, sourceContent: line.sourceContent ?? null }));
1490
1488
  }
1491
1489
  task.status = 'SUCCEEDED';
1492
1490
  task.resultEntityId = entity.id;
@@ -3425,14 +3423,20 @@ async function required(promise, message) { const value = await promise; if (!va
3425
3423
  function pick(input, keys) { return Object.fromEntries(keys.filter(key => key in input).map(key => [key, input[key]])); }
3426
3424
  function maxRisk(levels) { return levels.includes('HIGH') ? 'HIGH' : levels.includes('MEDIUM') ? 'MEDIUM' : levels.includes('LOW') ? 'LOW' : null; }
3427
3425
  function assertPurchaseDraft(draft) {
3428
- if (!String(draft.order.orderDate ?? '').trim())
3429
- throw new DomainError(DomainErrorCodes.INVALID_INPUT, '下单日期为必填项');
3430
- const contractAmount = decimalInput(draft.order.contractAmountCny, '采购订单总额必须为有效金额');
3431
- const paidAmount = decimalInput(draft.order.paidAmountCny ?? 0, '已付款必须为有效金额');
3432
- const refundedAmount = decimalInput(draft.order.refundedAmountCny ?? 0, '已退税必须为有效金额');
3426
+ if (!hasQueryValue(draft.supplier?.name))
3427
+ throw new DomainError(DomainErrorCodes.INVALID_INPUT, '供应商名称为必填项');
3428
+ if (!hasQueryValue(draft.order?.contractNo))
3429
+ throw new DomainError(DomainErrorCodes.INVALID_INPUT, '采购合同编号为必填项');
3430
+ if (!draft.lines.length)
3431
+ throw new DomainError(DomainErrorCodes.INVALID_INPUT, '采购合同必须包含至少一条商品');
3432
+ const contractAmount = optionalDecimalInput(draft.order.contractAmountCny, '采购订单总额必须为有效金额') ?? new Decimal(0);
3433
+ const paidAmount = optionalDecimalInput(draft.order.paidAmountCny, '已付款必须为有效金额') ?? new Decimal(0);
3434
+ const refundedAmount = optionalDecimalInput(draft.order.refundedAmountCny, '已退税必须为有效金额') ?? new Decimal(0);
3435
+ if (contractAmount.isNegative())
3436
+ throw new DomainError(DomainErrorCodes.INVALID_INPUT, '采购订单总额必须为有效金额');
3433
3437
  if (paidAmount.isNegative())
3434
3438
  throw new DomainError(DomainErrorCodes.INVALID_INPUT, '已付款不能为负数');
3435
- if (paidAmount.gt(contractAmount))
3439
+ if (hasQueryValue(draft.order.paidAmountCny) && hasQueryValue(draft.order.contractAmountCny) && paidAmount.gt(contractAmount))
3436
3440
  throw new DomainError(DomainErrorCodes.INVALID_INPUT, '已付款不能超过采购订单总额');
3437
3441
  if (refundedAmount.isNegative())
3438
3442
  throw new DomainError(DomainErrorCodes.INVALID_INPUT, '已退税不能为负数');
@@ -3442,28 +3446,32 @@ function assertPurchaseDraft(draft) {
3442
3446
  for (let index = 0; index < draft.lines.length; index += 1) {
3443
3447
  const line = draft.lines[index];
3444
3448
  const prefix = `第${index + 1}条商品`;
3445
- if (!hasQueryValue(line.refundRate))
3446
- throw new DomainError(DomainErrorCodes.INVALID_INPUT, `${prefix}退税率为必填项`);
3447
- if (!hasQueryValue(line.refundPolicyReference))
3448
- throw new DomainError(DomainErrorCodes.INVALID_INPUT, `${prefix}退税政策依据为必填项`);
3449
- if (!hasQueryValue(line.refundPolicyDate))
3450
- throw new DomainError(DomainErrorCodes.INVALID_INPUT, `${prefix}退税政策日期为必填项`);
3451
- if (String(line.refundPolicyDate) > String(draft.order.orderDate))
3449
+ if (!hasQueryValue(line.productName))
3450
+ throw new DomainError(DomainErrorCodes.INVALID_INPUT, `${prefix}名称为必填项`);
3451
+ const quantity = optionalDecimalInput(line.quantity, `${prefix}数量必须为有效数值`);
3452
+ const unitPrice = optionalDecimalInput(line.unitPriceTaxIncluded, `${prefix}含税单价必须为有效金额`);
3453
+ const lineAmount = optionalDecimalInput(line.lineAmountTaxIncluded, `${prefix}含税金额必须为有效金额`);
3454
+ validateOptionalTaxRate(line.vatRate, 'vatRate', '增值税率', { lineNumber: index + 1, productName: line.productName });
3455
+ const refundRate = validateOptionalTaxRate(line.refundRate, 'refundRate', '退税率', { lineNumber: index + 1, productName: line.productName });
3456
+ if (hasQueryValue(line.refundPolicyDate) && hasQueryValue(draft.order.orderDate) && String(line.refundPolicyDate) > String(draft.order.orderDate))
3452
3457
  throw new DomainError(DomainErrorCodes.INVALID_INPUT, `${prefix}退税政策日期不得晚于下单日期`);
3453
- if (!hasQueryValue(line.refundValidationMethod))
3454
- throw new DomainError(DomainErrorCodes.INVALID_INPUT, `${prefix}退税校验方式为必填项`);
3455
- if (!RefundValidationMethods.some(method => method === line.refundValidationMethod))
3458
+ if (hasQueryValue(line.refundValidationMethod) && !RefundValidationMethods.some(method => method === line.refundValidationMethod))
3456
3459
  throw new DomainError(DomainErrorCodes.INVALID_INPUT, `${prefix}退税校验方式无效`);
3457
3460
  validatePurchaseLineDiscriminators({ ...line, sourceType: line.sourceType ?? 'CONTRACT_EXTRACTED', hsCodeStatus: line.hsCodeStatus ?? 'UNCONFIRMED' });
3458
3461
  if (line.amountMismatch === true && line.amountMismatchConfirmed !== true)
3459
3462
  throw new DomainError(DomainErrorCodes.INVALID_INPUT, `${prefix}金额差异必须确认`);
3460
- const lineAmount = decimalInput(line.lineAmountTaxIncluded, `${prefix}含税金额必须为有效金额`);
3461
- const rates = validateTaxRates(line, { lineNumber: index + 1, productName: line.productName });
3462
- const vatRate = new Decimal(rates.vatRate);
3463
- const refundRate = new Decimal(rates.refundRate);
3464
- estimatedRefund = estimatedRefund.plus(lineAmount.div(vatRate.add(1)).mul(refundRate));
3463
+ if (quantity && quantity.lte(0))
3464
+ throw new DomainError(DomainErrorCodes.INVALID_INPUT, `${prefix}数量必须大于0`);
3465
+ if (unitPrice && unitPrice.isNegative())
3466
+ throw new DomainError(DomainErrorCodes.INVALID_INPUT, `${prefix}含税单价不能为负数`);
3467
+ if (lineAmount && lineAmount.isNegative())
3468
+ throw new DomainError(DomainErrorCodes.INVALID_INPUT, `${prefix}含税金额不能为负数`);
3469
+ if (lineAmount && refundRate) {
3470
+ const vatRate = validateOptionalTaxRate(line.vatRate, 'vatRate', '增值税率', { lineNumber: index + 1, productName: line.productName });
3471
+ estimatedRefund = estimatedRefund.plus(lineAmount.div((vatRate ?? new Decimal(0)).add(1)).mul(refundRate));
3472
+ }
3465
3473
  }
3466
- if (refundedAmount.gt(estimatedRefund))
3474
+ if (hasQueryValue(draft.order.refundedAmountCny) && refundedAmount.gt(estimatedRefund))
3467
3475
  throw new DomainError(DomainErrorCodes.INVALID_INPUT, '已退税不能超过预计退税总额');
3468
3476
  }
3469
3477
  function decimalInput(value, message) {
@@ -3474,6 +3482,25 @@ function decimalInput(value, message) {
3474
3482
  throw new DomainError(DomainErrorCodes.INVALID_INPUT, message);
3475
3483
  }
3476
3484
  }
3485
+ function optionalDecimalInput(value, message) {
3486
+ if (!hasQueryValue(value))
3487
+ return null;
3488
+ const parsed = decimalInput(value, message);
3489
+ if (!parsed.isFinite())
3490
+ throw new DomainError(DomainErrorCodes.INVALID_INPUT, message);
3491
+ return parsed;
3492
+ }
3493
+ function defaultDecimalText(value) {
3494
+ return optionalDecimalInput(value, '数值必须是有限金额')?.toString() ?? '0';
3495
+ }
3496
+ function validateOptionalTaxRate(value, field, label, context) {
3497
+ if (!hasQueryValue(value))
3498
+ return null;
3499
+ const parsed = decimalInput(value, `${label}必须是 0 至 1 之间的有限数值`);
3500
+ if (!parsed.isFinite() || parsed.lt(0) || parsed.gt(1))
3501
+ throw new DomainError(DomainErrorCodes.INVALID_TAX_RATE, `${label}必须是 0 至 1 之间的有限数值`, { field, value, ...context });
3502
+ return parsed;
3503
+ }
3477
3504
  function completeCustomerClauseAnalyses(draft) {
3478
3505
  return ClauseTypes.map(clauseType => {
3479
3506
  const clause = draft.clauses.find(item => item.clauseType === clauseType);