feat: 接口2组装加购结算报文,接口3接收采购合同
从表2取 push_status=0 拼文档报文,同 omsId 不拆批。 POST /api/oms/v1/purchase-contracts 按对方 fid 幂等落表3。
This commit is contained in:
parent
0379f78fe2
commit
abf26b4259
18
AGENTS.md
18
AGENTS.md
|
|
@ -21,6 +21,9 @@
|
||||||
| 接口1 收件契约 | `rpa_oms/app/inbound/sales_goods.py`、`envelope.py` | `rpa_oms/tests/test_sales_goods_receive.py::点名` |
|
| 接口1 收件契约 | `rpa_oms/app/inbound/sales_goods.py`、`envelope.py` | `rpa_oms/tests/test_sales_goods_receive.py::点名` |
|
||||||
| 接口1 Oracle 落表 | `rpa_oms/app/inbound/oracle_repo.py` | `rpa_oms/tests/test_oracle_repo.py::点名` |
|
| 接口1 Oracle 落表 | `rpa_oms/app/inbound/oracle_repo.py` | `rpa_oms/tests/test_oracle_repo.py::点名` |
|
||||||
| 接口1 入口 / 鉴权 | `rpa_oms/app/main.py`、`auth.py` | 同上;真库见 `rpa_oms/README.md` Windows 段 |
|
| 接口1 入口 / 鉴权 | `rpa_oms/app/main.py`、`auth.py` | 同上;真库见 `rpa_oms/README.md` Windows 段 |
|
||||||
|
| 接口2 取数组装 | `rpa_oms/app/outbound/cart_settlement.py` | `rpa_oms/tests/test_cart_settlement_outbound.py::test_assembles_unpushed_rows_as_document_envelope` |
|
||||||
|
| 接口3 收件契约 | `rpa_oms/app/inbound/purchase_contract.py` | `rpa_oms/tests/test_purchase_contract_receive.py::点名` |
|
||||||
|
| 接口3 Oracle 落表 | `rpa_oms/app/inbound/oracle_contract_repo.py` | `rpa_oms/tests/test_oracle_contract_repo.py::点名` |
|
||||||
| 选品规则 / 拆量 | `rpa_selection/rules.py`、`pipeline.py` | `rpa_selection/tests/test_pipeline.py::test_split_across_malls_is_success` |
|
| 选品规则 / 拆量 | `rpa_selection/rules.py`、`pipeline.py` | `rpa_selection/tests/test_pipeline.py::test_split_across_malls_is_success` |
|
||||||
| 选品班次落表 | `rpa_selection/job.py`、`persist.py`、`repo.py` | `rpa_selection/tests/test_job.py::test_job_searches_barcode_not_approval_no` |
|
| 选品班次落表 | `rpa_selection/job.py`、`persist.py`、`repo.py` | `rpa_selection/tests/test_job.py::test_job_searches_barcode_not_approval_no` |
|
||||||
| 合同页面步骤 | `rpa_contract/contract_flow_steps.py` 里对应函数 | `ACCEPT SUMMARY` 或 `rpa_contract/screenshots/` 下 `ACCEPT.txt` |
|
| 合同页面步骤 | `rpa_contract/contract_flow_steps.py` 里对应函数 | `ACCEPT SUMMARY` 或 `rpa_contract/screenshots/` 下 `ACCEPT.txt` |
|
||||||
|
|
@ -37,7 +40,7 @@
|
||||||
|
|
||||||
## 接口1 落点(订单中心)
|
## 接口1 落点(订单中心)
|
||||||
|
|
||||||
订单中心四接口,本期只开放接口 1:销售商品对码数据接收。`POST /api/oms/v1/sales-goods`,鉴权 header `X-Oms-Token`(`OMS_INBOUND_TOKEN`)。落表 `rpa_sales_goods_mapping_data`,`deal_flag=0`,等选品作业捞;**请求里不跑选品**。
|
订单中心四接口,本期开放接口 1、3 收件。鉴权 header `X-Oms-Token`(`OMS_INBOUND_TOKEN`)。接口 1:`POST /api/oms/v1/sales-goods`,落表 `rpa_sales_goods_mapping_data`,`deal_flag=0`,**请求里不跑选品**。接口 3:`POST /api/oms/v1/purchase-contracts`,落表 `rpa_purchase_contract_data`,`process_status=0`,按对方 `fid` 幂等,**请求里不录合同**。接口 2 只从表 2 取 `push_status=0` 组装报文,路径仍空,不发 HTTP。
|
||||||
|
|
||||||
运行时只落 Oracle。单测用内存替身,不连库。服务端口 **8001**。
|
运行时只落 Oracle。单测用内存替身,不连库。服务端口 **8001**。
|
||||||
|
|
||||||
|
|
@ -52,11 +55,15 @@
|
||||||
| 字段不过 | `_parse_row` | `REJECTED`,不入库 |
|
| 字段不过 | `_parse_row` | `REJECTED`,不入库 |
|
||||||
| 混批 | `post_sales_goods` | 逐条有成功/重复/拒绝时 HTTP 仍 200,看 `results[].resultCode` |
|
| 混批 | `post_sales_goods` | 逐条有成功/重复/拒绝时 HTTP 仍 200,看 `results[].resultCode` |
|
||||||
|
|
||||||
`omsId` / `saleQty` / `planPurchaseQty` 必须是 JSON 整数,不把 `"10"` 当 10。Windows 连真库测;Instant Client 位数与 Python 一致。
|
`omsId` / `saleQty` / `planPurchaseQty` / 接口 3 的 `fid` 必须是 JSON 整数,不把 `"10"` 当 10。Windows 连真库测;Instant Client 位数与 Python 一致。
|
||||||
|
|
||||||
验收(内存、不连库):`cd rpa_oms` → `uv run pytest tests/test_sales_goods_receive.py::点名 -q`。真库:`GET /health` 后把 `examples/sales_goods_batch.json` POST 两次(第一次两条 `ACCEPTED`,第二次两条 `DUPLICATE`)。
|
接口 2:`load_cart_settlement_envelope` 捞 `push_status=0`,同一 `omsId` 整组进一批,上限 500;金额四位小数字符串。不改 `push_status`,不 POST 订单中心。
|
||||||
|
|
||||||
修接口1不要顺手改 `rpa_selection/`、`rpa_cart/`、`rpa_contract/`、`rpa_rebate/`。接口 2–4 未开放,不要先写。
|
接口 3:`receive_purchase_contracts` 按 `fid` 幂等;同批 `fid` 再现 → `CONFLICT`;`contractEntryStatus=0` 必须带 `contractNo`;结束日不得早于开始日。`INSERT` 绑 `fid`。
|
||||||
|
|
||||||
|
验收(内存、不连库):`cd rpa_oms` → `uv run pytest tests/test_sales_goods_receive.py::点名 -q`。真库:`GET /health` 后把 `examples/sales_goods_batch.json` POST 两次(第一次两条 `ACCEPTED`,第二次两条 `DUPLICATE`)。接口 3 点名 `tests/test_purchase_contract_receive.py`;真库 POST `examples/purchase_contract_batch.json` 两次。接口 2 点名 `tests/test_cart_settlement_outbound.py::test_assembles_unpushed_rows_as_document_envelope`。
|
||||||
|
|
||||||
|
修接口1不要顺手改 `rpa_selection/`、`rpa_cart/`、`rpa_contract/`、`rpa_rebate/`。接口 4 未开放;接口 2 只组装不推送。
|
||||||
|
|
||||||
## 选品落点
|
## 选品落点
|
||||||
|
|
||||||
|
|
@ -114,6 +121,8 @@
|
||||||
## 验收
|
## 验收
|
||||||
|
|
||||||
- 接口1:点名 `rpa_oms/tests/test_sales_goods_receive.py` 或 `test_oracle_repo.py`;禁止为「显得全面」跑整包再去连真库。
|
- 接口1:点名 `rpa_oms/tests/test_sales_goods_receive.py` 或 `test_oracle_repo.py`;禁止为「显得全面」跑整包再去连真库。
|
||||||
|
- 接口2:点名 `rpa_oms/tests/test_cart_settlement_outbound.py::test_assembles_unpushed_rows_as_document_envelope`。
|
||||||
|
- 接口3:点名 `rpa_oms/tests/test_purchase_contract_receive.py` 或 `test_oracle_contract_repo.py`。
|
||||||
- 选品:点名 `rpa_selection/tests/test_pipeline.py::test_split_across_malls_is_success` 或 `test_rules.py::test_margin_exactly_4_percent_passes`。
|
- 选品:点名 `rpa_selection/tests/test_pipeline.py::test_split_across_malls_is_success` 或 `test_rules.py::test_margin_exactly_4_percent_passes`。
|
||||||
- 合同页:只读控制台 `ACCEPT` 或 `rpa_contract/screenshots/` 下 `ACCEPT.txt`。
|
- 合同页:只读控制台 `ACCEPT` 或 `rpa_contract/screenshots/` 下 `ACCEPT.txt`。
|
||||||
- 单测只跑点名用例。禁止跑整份带页面等待的套件当回归。
|
- 单测只跑点名用例。禁止跑整份带页面等待的套件当回归。
|
||||||
|
|
@ -123,4 +132,5 @@
|
||||||
- 不要扫根目录,不要通读 `docs/`。需要背景就点名打开一节。
|
- 不要扫根目录,不要通读 `docs/`。需要背景就点名打开一节。
|
||||||
- 不要把 Oracle 密码写进仓库、README、示例 JSON。
|
- 不要把 Oracle 密码写进仓库、README、示例 JSON。
|
||||||
- 不要在接口1 请求路径里跑选品。
|
- 不要在接口1 请求路径里跑选品。
|
||||||
|
- 不要在接口3 请求路径里录合同。
|
||||||
- 不要为「显得全面」多开文件、重跑、看图。落点外的改动会制造反工。
|
- 不要为「显得全面」多开文件、重跑、看图。落点外的改动会制造反工。
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@
|
||||||
|
|
||||||
| 口 | 方向 | 底表 | 状态 |
|
| 口 | 方向 | 底表 | 状态 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 1 | 订单中心 → 本方 | `rpa_sales_goods_mapping_data` | 进行中(`rpa_oms/`) |
|
| 1 | 订单中心 → 本方 | `rpa_sales_goods_mapping_data` | `POST /api/oms/v1/sales-goods` |
|
||||||
| 2 | 本方 → 订单中心 | `rpa_cart_settlement_goods_data` | 选品已落表,push_status=0,路径仍空 |
|
| 2 | 本方 → 订单中心 | `rpa_cart_settlement_goods_data` | 取 `push_status=0` 组装报文;路径仍空,不发 HTTP |
|
||||||
| 3 | 订单中心 → 本方 | `rpa_purchase_contract_data` | 复用接口 1 接收壳 |
|
| 3 | 订单中心 → 本方 | `rpa_purchase_contract_data` | `POST /api/oms/v1/purchase-contracts` |
|
||||||
| 4 | 本方 → 订单中心 | `rpa_contract_agreement_result` | 路径空,先造数 |
|
| 4 | 本方 → 订单中心 | `rpa_contract_agreement_result` | 路径空,先造数 |
|
||||||
|
|
||||||
作业互不串联:选品读表 1,合同/返利读表 3。HTTP 收件不同步跑 RPA。
|
作业互不串联:选品读表 1,合同/返利读表 3。HTTP 收件不同步跑 RPA。
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
| 目录 | 身份 |
|
| 目录 | 身份 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `rpa_oms/` | 四个 HTTP 口,一个 FastAPI。先做接口 1 |
|
| `rpa_oms/` | 四个 HTTP 口,一个 FastAPI。接口 1、3 收件;接口 2 组装报文 |
|
||||||
| `rpa_selection/` | 选品班。读表 1 `deal_flag=0`,双平台选品,写表 2 |
|
| `rpa_selection/` | 选品班。读表 1 `deal_flag=0`,双平台选品,写表 2 |
|
||||||
| `rpa_cart/` | 加购(未做)+ 1 号药城支付落表。表 2 → 口 2 仍未推 |
|
| `rpa_cart/` | 加购(未做)+ 1 号药城支付落表。表 2 → 口 2 仍未推 |
|
||||||
| `rpa_contract/` | 合同班。页面录入在本包;表 3 合单、接口 4 尚未接 |
|
| `rpa_contract/` | 合同班。页面录入在本包;表 3 合单、接口 4 尚未接 |
|
||||||
|
|
|
||||||
|
|
@ -6,19 +6,28 @@
|
||||||
|
|
||||||
| 口 | 方向 | 底表 | 代码 |
|
| 口 | 方向 | 底表 | 代码 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 1 | 订单中心 → 本方 | `rpa_sales_goods_mapping_data` | 已有 `app/inbound/sales_goods.py`,`POST /api/oms/v1/sales-goods` |
|
| 1 | 订单中心 → 本方 | `rpa_sales_goods_mapping_data` | `POST /api/oms/v1/sales-goods` |
|
||||||
| 2 | 本方 → 订单中心 | `rpa_cart_settlement_goods_data` | 未做。将来 `app/outbound/`,由 `rpa_cart` 调用 |
|
| 2 | 本方 → 订单中心 | `rpa_cart_settlement_goods_data` | `app/outbound/cart_settlement.py` 取 `push_status=0` 组装报文。路径仍空,不发 HTTP |
|
||||||
| 3 | 订单中心 → 本方 | `rpa_purchase_contract_data` | 未做。将来 `app/inbound/`,复用接口 1 信封/鉴权 |
|
| 3 | 订单中心 → 本方 | `rpa_purchase_contract_data` | `POST /api/oms/v1/purchase-contracts`,按对方 `fid` 幂等 |
|
||||||
| 4 | 本方 → 订单中心 | `rpa_contract_agreement_result` | 未做。将来 `app/outbound/`,由合同/返利班调用 |
|
| 4 | 本方 → 订单中心 | `rpa_contract_agreement_result` | 未做。将来由合同/返利班调用 |
|
||||||
|
|
||||||
本期只开放接口 1:
|
入站(1、3):
|
||||||
|
|
||||||
- 路径:`POST /api/oms/v1/sales-goods`
|
|
||||||
- 鉴权:header `X-Oms-Token`,环境变量 `OMS_INBOUND_TOKEN`
|
- 鉴权:header `X-Oms-Token`,环境变量 `OMS_INBOUND_TOKEN`
|
||||||
- 落表:`rpa_sales_goods_mapping_data`(`deal_flag=0`)
|
- 信封:`{"data":[...]}`,一批最多 100 条
|
||||||
- 请求里不跑选品。选品班次在 `rpa_selection/`,按条形码搜两平台、四级仓序拆量后写 `rpa_cart_settlement_goods_data`
|
- 接口 1 落表 `deal_flag=0`,请求里不跑选品
|
||||||
|
- 接口 3 落表 `process_status=0`,请求里不录合同
|
||||||
|
|
||||||
运行时只落 Oracle。单测用内存替身,不连库:`uv run pytest -v`。
|
接口 2 出站组装:`load_cart_settlement_envelope(repo)`,同一 `omsId` 不拆批,一批最多 500 条。不改 `push_status`。
|
||||||
|
|
||||||
|
```python
|
||||||
|
from app.outbound.cart_settlement import load_cart_settlement_envelope
|
||||||
|
from app.outbound.oracle_repo import OracleCartSettlementRepo
|
||||||
|
|
||||||
|
envelope = load_cart_settlement_envelope(OracleCartSettlementRepo.from_env())
|
||||||
|
```
|
||||||
|
|
||||||
|
运行时只落 / 读 Oracle。单测用内存替身,不连库:`uv run pytest -v`。
|
||||||
|
|
||||||
## 真库
|
## 真库
|
||||||
|
|
||||||
|
|
@ -66,3 +75,12 @@ curl.exe -sS http://127.0.0.1:8001/api/oms/v1/sales-goods `
|
||||||
-H "X-Oms-Token: dev-oms-token" `
|
-H "X-Oms-Token: dev-oms-token" `
|
||||||
--data-binary "@examples/sales_goods_batch.json"
|
--data-binary "@examples/sales_goods_batch.json"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
接口 3 同样 POST 两次 `examples/purchase_contract_batch.json`:第一次两条 `ACCEPTED`(`process_status=0`),第二次两条 `DUPLICATE`。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
curl.exe -sS http://127.0.0.1:8001/api/oms/v1/purchase-contracts `
|
||||||
|
-H "Content-Type: application/json" `
|
||||||
|
-H "X-Oms-Token: dev-oms-token" `
|
||||||
|
--data-binary "@examples/purchase_contract_batch.json"
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
"""入站收件:拆信封、逐条校验、按 omsId 幂等落表。"""
|
"""入站收件:拆信封、逐条校验。接口 1 按 omsId 幂等,接口 3 按 fid 幂等。"""
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"""批量信封。缺 data、空数组、超过 100 条:整包 422,一条都不落。
|
"""批量信封。缺 data、空数组、超过 100 条:整包 422,一条都不落。
|
||||||
|
|
||||||
单条字段错误不走这里,由 sales_goods 逐条 REJECTED,HTTP 仍 200。
|
单条字段错误不走这里,由各收件函数逐条 REJECTED,HTTP 仍 200。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
MAX_BATCH = 100
|
MAX_BATCH = 100
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from .repo import DuplicateOmsId
|
from .repo import DuplicateFid, DuplicateOmsId
|
||||||
|
|
||||||
|
|
||||||
class MemorySalesGoodsRepo:
|
class MemorySalesGoodsRepo:
|
||||||
|
|
@ -23,3 +23,21 @@ class MemorySalesGoodsRepo:
|
||||||
|
|
||||||
def count(self) -> int:
|
def count(self) -> int:
|
||||||
return len(self._rows)
|
return len(self._rows)
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryPurchaseContractRepo:
|
||||||
|
def __init__(self):
|
||||||
|
self._rows: dict[int, dict] = {}
|
||||||
|
|
||||||
|
def insert(self, record: dict) -> None:
|
||||||
|
fid = record["fid"]
|
||||||
|
if fid in self._rows:
|
||||||
|
raise DuplicateFid(fid)
|
||||||
|
self._rows[fid] = dict(record)
|
||||||
|
|
||||||
|
def get(self, fid: int) -> dict | None:
|
||||||
|
row = self._rows.get(fid)
|
||||||
|
return dict(row) if row is not None else None
|
||||||
|
|
||||||
|
def count(self) -> int:
|
||||||
|
return len(self._rows)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
"""接口 3 Oracle 落表。fid 是订单中心来源序号,INSERT 必须绑这一列。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
from .oracle_repo import _ensure_client, _ora_code
|
||||||
|
from .repo import DuplicateFid, RepoUnavailable
|
||||||
|
|
||||||
|
_INSERT_SQL = """
|
||||||
|
INSERT INTO rpa_purchase_contract_data (
|
||||||
|
fid, oms_id, supplier_id, supplier_name,
|
||||||
|
contract_sign_date, contract_start_date, contract_end_date,
|
||||||
|
payment_term, goods_id, goods_name, reg_no, spec, unit,
|
||||||
|
qty, purchase_tax_rate, unit_price, goods_source_id,
|
||||||
|
purchaser_id, purchaser_name, planner_id, planner_name,
|
||||||
|
contract_entry_status, contract_no, org_code, customer_type,
|
||||||
|
goods_mdm_code, supplier_mdm_code,
|
||||||
|
process_status, create_time
|
||||||
|
) VALUES (
|
||||||
|
:fid, :oms_id, :supplier_id, :supplier_name,
|
||||||
|
:contract_sign_date, :contract_start_date, :contract_end_date,
|
||||||
|
:payment_term, :goods_id, :goods_name, :reg_no, :spec, :unit,
|
||||||
|
:qty, :purchase_tax_rate, :unit_price, :goods_source_id,
|
||||||
|
:purchaser_id, :purchaser_name, :planner_id, :planner_name,
|
||||||
|
:contract_entry_status, :contract_no, :org_code, :customer_type,
|
||||||
|
:goods_mdm_code, :supplier_mdm_code,
|
||||||
|
0, SYSTIMESTAMP
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
_BIND_KEYS = (
|
||||||
|
"fid",
|
||||||
|
"oms_id",
|
||||||
|
"supplier_id",
|
||||||
|
"supplier_name",
|
||||||
|
"contract_sign_date",
|
||||||
|
"contract_start_date",
|
||||||
|
"contract_end_date",
|
||||||
|
"payment_term",
|
||||||
|
"goods_id",
|
||||||
|
"goods_name",
|
||||||
|
"reg_no",
|
||||||
|
"spec",
|
||||||
|
"unit",
|
||||||
|
"qty",
|
||||||
|
"purchase_tax_rate",
|
||||||
|
"unit_price",
|
||||||
|
"goods_source_id",
|
||||||
|
"purchaser_id",
|
||||||
|
"purchaser_name",
|
||||||
|
"planner_id",
|
||||||
|
"planner_name",
|
||||||
|
"contract_entry_status",
|
||||||
|
"contract_no",
|
||||||
|
"org_code",
|
||||||
|
"customer_type",
|
||||||
|
"goods_mdm_code",
|
||||||
|
"supplier_mdm_code",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OraclePurchaseContractRepo:
|
||||||
|
def __init__(self, connect: Callable):
|
||||||
|
self._connect = connect
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_env(cls) -> "OraclePurchaseContractRepo":
|
||||||
|
user = os.getenv("RPA_ORACLE_USER", "")
|
||||||
|
password = os.getenv("RPA_ORACLE_PASSWORD", "")
|
||||||
|
host = os.getenv("RPA_ORACLE_HOST", "")
|
||||||
|
if not user or not password or not host:
|
||||||
|
raise RepoUnavailable("未配置 RPA_ORACLE_HOST / RPA_ORACLE_USER / RPA_ORACLE_PASSWORD")
|
||||||
|
port = os.getenv("RPA_ORACLE_PORT", "1521")
|
||||||
|
service = os.getenv("RPA_ORACLE_SERVICE", "rpa")
|
||||||
|
dsn = f"{host}:{port}/{service}"
|
||||||
|
|
||||||
|
def connect():
|
||||||
|
_ensure_client()
|
||||||
|
import oracledb
|
||||||
|
|
||||||
|
return oracledb.connect(user=user, password=password, dsn=dsn)
|
||||||
|
|
||||||
|
return cls(connect=connect)
|
||||||
|
|
||||||
|
def insert(self, record: dict) -> None:
|
||||||
|
conn = self._connect()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(_INSERT_SQL, {key: record[key] for key in _BIND_KEYS})
|
||||||
|
conn.commit()
|
||||||
|
except DuplicateFid:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
try:
|
||||||
|
conn.rollback()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if _ora_code(exc) == 1:
|
||||||
|
raise DuplicateFid(record["fid"]) from exc
|
||||||
|
raise RepoUnavailable(str(exc)) from exc
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
"""接口 3 逐条校验与落表。不跑合同作业。
|
||||||
|
|
||||||
|
入参 camelCase,落表 snake_case。幂等键是对方 fid。
|
||||||
|
请求里不录合同。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
|
from .repo import DuplicateFid
|
||||||
|
from .sales_goods import _parse_decimal, _parse_int, _parse_oms_id, _parse_str
|
||||||
|
|
||||||
|
_STRING_FIELDS = (
|
||||||
|
("supplierId", "supplier_id", 20),
|
||||||
|
("supplierName", "supplier_name", 200),
|
||||||
|
("goodsId", "goods_id", 20),
|
||||||
|
("goodsName", "goods_name", 200),
|
||||||
|
("regNo", "reg_no", 100),
|
||||||
|
("spec", "spec", 100),
|
||||||
|
("unit", "unit", 20),
|
||||||
|
("goodsSourceId", "goods_source_id", 20),
|
||||||
|
("purchaserId", "purchaser_id", 20),
|
||||||
|
("purchaserName", "purchaser_name", 100),
|
||||||
|
("plannerId", "planner_id", 20),
|
||||||
|
("plannerName", "planner_name", 100),
|
||||||
|
("orgCode", "org_code", 10),
|
||||||
|
("goodsMdmCode", "goods_mdm_code", 100),
|
||||||
|
("supplierMdmCode", "supplier_mdm_code", 100),
|
||||||
|
)
|
||||||
|
|
||||||
|
_DATE_FIELDS = (
|
||||||
|
("contractSignDate", "contract_sign_date"),
|
||||||
|
("contractStartDate", "contract_start_date"),
|
||||||
|
("contractEndDate", "contract_end_date"),
|
||||||
|
)
|
||||||
|
|
||||||
|
_DECIMAL_FIELDS = (
|
||||||
|
("qty", "qty"),
|
||||||
|
("purchaseTaxRate", "purchase_tax_rate"),
|
||||||
|
("unitPrice", "unit_price"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def receive_purchase_contracts(rows: list, repo) -> list[dict]:
|
||||||
|
seen: set[int] = set()
|
||||||
|
results: list[dict] = []
|
||||||
|
for row in rows:
|
||||||
|
results.append(_receive_one(row, repo, seen))
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def _receive_one(row: object, repo, seen: set[int]) -> dict:
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
return _result(None, "REJECTED", "明细必须是对象")
|
||||||
|
|
||||||
|
fid, fid_error = _parse_fid(row.get("fid"))
|
||||||
|
if fid is not None and fid in seen:
|
||||||
|
return _result(fid, "CONFLICT", "同一请求中 fid 重复")
|
||||||
|
if fid is not None:
|
||||||
|
seen.add(fid)
|
||||||
|
|
||||||
|
parsed, error = _parse_row(row, fid, fid_error)
|
||||||
|
if error:
|
||||||
|
_camel, message = error
|
||||||
|
return _result(row.get("fid"), "REJECTED", message)
|
||||||
|
|
||||||
|
try:
|
||||||
|
repo.insert(parsed)
|
||||||
|
except DuplicateFid:
|
||||||
|
return _result(parsed["fid"], "DUPLICATE", "序号已存在,未重复入库")
|
||||||
|
|
||||||
|
return _result(parsed["fid"], "ACCEPTED", "采购合同数据已接收,等待处理")
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_row(row: dict, fid: int | None, fid_error: str | None):
|
||||||
|
if fid_error:
|
||||||
|
return None, ("fid", fid_error)
|
||||||
|
|
||||||
|
oms_id, oms_error = _parse_oms_id(row.get("omsId"))
|
||||||
|
if oms_error:
|
||||||
|
return None, ("omsId", oms_error)
|
||||||
|
|
||||||
|
record: dict = {"fid": fid, "oms_id": oms_id, "process_status": 0}
|
||||||
|
for camel, snake, max_len in _STRING_FIELDS:
|
||||||
|
value, error = _parse_str(row.get(camel), camel, max_len)
|
||||||
|
if error:
|
||||||
|
return None, (camel, error)
|
||||||
|
record[snake] = value
|
||||||
|
|
||||||
|
for camel, snake in _DATE_FIELDS:
|
||||||
|
value, error = _parse_date(row.get(camel), camel)
|
||||||
|
if error:
|
||||||
|
return None, (camel, error)
|
||||||
|
record[snake] = value
|
||||||
|
|
||||||
|
if record["contract_end_date"] < record["contract_start_date"]:
|
||||||
|
return None, ("contractEndDate", "contractEndDate 不得早于 contractStartDate")
|
||||||
|
|
||||||
|
payment_term, error = _parse_int(row.get("paymentTerm"), "paymentTerm")
|
||||||
|
if error:
|
||||||
|
return None, ("paymentTerm", error)
|
||||||
|
record["payment_term"] = payment_term
|
||||||
|
|
||||||
|
for camel, snake in _DECIMAL_FIELDS:
|
||||||
|
value, error = _parse_decimal(row.get(camel), camel)
|
||||||
|
if error:
|
||||||
|
return None, (camel, error)
|
||||||
|
record[snake] = value
|
||||||
|
|
||||||
|
customer_type, error = _parse_int(row.get("customerType"), "customerType")
|
||||||
|
if error:
|
||||||
|
return None, ("customerType", error)
|
||||||
|
record["customer_type"] = customer_type
|
||||||
|
|
||||||
|
entry_status, error = _parse_entry_status(row.get("contractEntryStatus"))
|
||||||
|
if error:
|
||||||
|
return None, ("contractEntryStatus", error)
|
||||||
|
record["contract_entry_status"] = entry_status
|
||||||
|
|
||||||
|
contract_no, error = _parse_contract_no(row.get("contractNo"), entry_status)
|
||||||
|
if error:
|
||||||
|
return None, ("contractNo", error)
|
||||||
|
record["contract_no"] = contract_no
|
||||||
|
|
||||||
|
return record, None
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_fid(value) -> tuple[int | None, str | None]:
|
||||||
|
if value is None or value == "":
|
||||||
|
return None, "缺少必填字段 fid"
|
||||||
|
if type(value) is int and 0 < value < 10**19:
|
||||||
|
return value, None
|
||||||
|
return None, "fid 必须是整数"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_date(value, camel: str) -> tuple[date | None, str | None]:
|
||||||
|
if value is None or value == "":
|
||||||
|
return None, f"缺少必填字段 {camel}"
|
||||||
|
if isinstance(value, datetime):
|
||||||
|
return value.date(), None
|
||||||
|
if isinstance(value, date):
|
||||||
|
return value, None
|
||||||
|
if not isinstance(value, str):
|
||||||
|
return None, f"{camel} 必须是 YYYY-MM-DD"
|
||||||
|
try:
|
||||||
|
return date.fromisoformat(value.strip()), None
|
||||||
|
except ValueError:
|
||||||
|
return None, f"{camel} 必须是 YYYY-MM-DD"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_entry_status(value) -> tuple[int | None, str | None]:
|
||||||
|
if value is None or value == "":
|
||||||
|
return None, None
|
||||||
|
if type(value) is int and value in (0, 1):
|
||||||
|
return value, None
|
||||||
|
return None, "contractEntryStatus 必须是 0 或 1"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_contract_no(value, entry_status: int | None) -> tuple[str | None, str | None]:
|
||||||
|
if entry_status == 0:
|
||||||
|
return _parse_str(value, "contractNo", 40)
|
||||||
|
if value is None or value == "":
|
||||||
|
return None, None
|
||||||
|
return _parse_str(value, "contractNo", 40)
|
||||||
|
|
||||||
|
|
||||||
|
def _result(fid, result_code: str, message: str) -> dict:
|
||||||
|
return {"fid": fid, "resultCode": result_code, "message": message}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"""收件仓库契约。
|
"""收件仓库契约。
|
||||||
|
|
||||||
DuplicateOmsId:oms_id 已存在,调用方回 DUPLICATE,不更新。
|
DuplicateOmsId:oms_id 已存在,调用方回 DUPLICATE,不更新。
|
||||||
|
DuplicateFid:接口 3 来源 fid 已存在,调用方回 DUPLICATE,不更新。
|
||||||
RepoUnavailable:连不上库或未配置,整包 HTTP 503,一条都不落。
|
RepoUnavailable:连不上库或未配置,整包 HTTP 503,一条都不落。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -10,6 +11,11 @@ class DuplicateOmsId(Exception):
|
||||||
self.oms_id = oms_id
|
self.oms_id = oms_id
|
||||||
|
|
||||||
|
|
||||||
|
class DuplicateFid(Exception):
|
||||||
|
def __init__(self, fid: int):
|
||||||
|
self.fid = fid
|
||||||
|
|
||||||
|
|
||||||
class RepoUnavailable(Exception):
|
class RepoUnavailable(Exception):
|
||||||
def __init__(self, message: str = "数据库不可用"):
|
def __init__(self, message: str = "数据库不可用"):
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
"""订单中心接口服务。本期只开放接口 1 收件。
|
"""订单中心接口服务。本期开放接口 1、3 收件;接口 2 只在 outbound 组装报文。
|
||||||
|
|
||||||
路径:POST /api/oms/v1/sales-goods
|
路径:
|
||||||
|
- POST /api/oms/v1/sales-goods
|
||||||
|
- POST /api/oms/v1/purchase-contracts
|
||||||
鉴权:X-Oms-Token
|
鉴权:X-Oms-Token
|
||||||
落表:rpa_sales_goods_mapping_data,deal_flag=0
|
请求里不跑选品、不录合同。
|
||||||
请求里不跑选品。
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
@ -17,6 +18,7 @@ from .local_env import load_local_env
|
||||||
|
|
||||||
load_local_env()
|
load_local_env()
|
||||||
from .inbound.envelope import EnvelopeError, parse_envelope
|
from .inbound.envelope import EnvelopeError, parse_envelope
|
||||||
|
from .inbound.purchase_contract import receive_purchase_contracts
|
||||||
from .inbound.repo import RepoUnavailable
|
from .inbound.repo import RepoUnavailable
|
||||||
from .inbound.sales_goods import receive_sales_goods
|
from .inbound.sales_goods import receive_sales_goods
|
||||||
|
|
||||||
|
|
@ -30,6 +32,12 @@ def get_repo():
|
||||||
return OracleSalesGoodsRepo.from_env()
|
return OracleSalesGoodsRepo.from_env()
|
||||||
|
|
||||||
|
|
||||||
|
def get_contract_repo():
|
||||||
|
from .inbound.oracle_contract_repo import OraclePurchaseContractRepo
|
||||||
|
|
||||||
|
return OraclePurchaseContractRepo.from_env()
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
def health():
|
def health():
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
@ -41,6 +49,27 @@ def post_sales_goods(
|
||||||
x_oms_token: Annotated[str | None, Header()] = None,
|
x_oms_token: Annotated[str | None, Header()] = None,
|
||||||
repo=Depends(get_repo),
|
repo=Depends(get_repo),
|
||||||
):
|
):
|
||||||
|
return _post_inbound(
|
||||||
|
payload,
|
||||||
|
x_oms_token,
|
||||||
|
receive=lambda rows: receive_sales_goods(rows, repo),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/oms/v1/purchase-contracts")
|
||||||
|
def post_purchase_contracts(
|
||||||
|
payload: dict[str, Any] | None = None,
|
||||||
|
x_oms_token: Annotated[str | None, Header()] = None,
|
||||||
|
repo=Depends(get_contract_repo),
|
||||||
|
):
|
||||||
|
return _post_inbound(
|
||||||
|
payload,
|
||||||
|
x_oms_token,
|
||||||
|
receive=lambda rows: receive_purchase_contracts(rows, repo),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _post_inbound(payload, x_oms_token, receive):
|
||||||
if not token_ok(x_oms_token):
|
if not token_ok(x_oms_token):
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=401,
|
status_code=401,
|
||||||
|
|
@ -54,7 +83,7 @@ def post_sales_goods(
|
||||||
content={"code": 422, "msg": exc.message, "results": []},
|
content={"code": 422, "msg": exc.message, "results": []},
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
results = receive_sales_goods(rows, repo)
|
results = receive(rows)
|
||||||
except RepoUnavailable as exc:
|
except RepoUnavailable as exc:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=503,
|
status_code=503,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
"""出站:从表组装订单中心报文。本期接口 2 只取数组装,不发 HTTP。"""
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
"""接口 2:从表 2 取未推行,组装文档报文。
|
||||||
|
|
||||||
|
同一 omsId 的加购行必须在同一批 data 里。一批最多 500 条。
|
||||||
|
不改 push_status,不向订单中心发 HTTP(路径仍空)。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
from decimal import Decimal, ROUND_HALF_UP
|
||||||
|
|
||||||
|
MAX_BATCH = 500
|
||||||
|
|
||||||
|
|
||||||
|
def load_cart_settlement_envelope(repo, limit: int = MAX_BATCH) -> dict:
|
||||||
|
return build_cart_settlement_envelope(repo.list_unpushed(), limit=limit)
|
||||||
|
|
||||||
|
|
||||||
|
def build_cart_settlement_envelope(rows: list[dict], limit: int = MAX_BATCH) -> dict:
|
||||||
|
chosen = _take_complete_groups(rows, limit)
|
||||||
|
return {"data": [_to_item(row) for row in chosen]}
|
||||||
|
|
||||||
|
|
||||||
|
def _take_complete_groups(rows: list[dict], limit: int) -> list[dict]:
|
||||||
|
groups: OrderedDict = OrderedDict()
|
||||||
|
for row in rows:
|
||||||
|
groups.setdefault(row["oms_id"], []).append(row)
|
||||||
|
batch: list[dict] = []
|
||||||
|
for group in groups.values():
|
||||||
|
if len(batch) + len(group) > limit and batch:
|
||||||
|
break
|
||||||
|
batch.extend(group)
|
||||||
|
return batch
|
||||||
|
|
||||||
|
|
||||||
|
def _to_item(row: dict) -> dict:
|
||||||
|
return {
|
||||||
|
"fid": _as_int(row["fid"]),
|
||||||
|
"operateType": _as_int(row["operate_type"]),
|
||||||
|
"omsId": _as_int(row["oms_id"]),
|
||||||
|
"platformType": _as_int(row["platform_type"]),
|
||||||
|
"supplierName": row["supplier_name"],
|
||||||
|
"barCode": row["bar_code"],
|
||||||
|
"purchasePrice": _money4(row["purchase_price"]),
|
||||||
|
"purchaseQty": _as_int(row["purchase_qty"]),
|
||||||
|
"purchaseTotal": _money4(row["purchase_total"]),
|
||||||
|
"status": _as_int(row["status"]) if row.get("status") is not None else None,
|
||||||
|
"failReason": row.get("fail_reason"),
|
||||||
|
"payOrderNo": row.get("pay_order_no"),
|
||||||
|
"payOrderDtlId": row.get("pay_order_dtl_id"),
|
||||||
|
"orgCode": row.get("org_code"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _as_int(value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return int(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _money4(value) -> str:
|
||||||
|
amount = Decimal(str(value)).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
|
||||||
|
return f"{amount:.4f}"
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
"""内存表 2。只给单测用。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryCartSettlementRepo:
|
||||||
|
def __init__(self, rows: list[dict] | None = None):
|
||||||
|
self._rows = [dict(row) for row in (rows or [])]
|
||||||
|
|
||||||
|
def list_unpushed(self) -> list[dict]:
|
||||||
|
return [dict(row) for row in self._rows if row.get("push_status", 0) == 0]
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
"""接口 2:从表 2 读取 push_status=0。不更新推送状态。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
from ..inbound.oracle_repo import _ensure_client
|
||||||
|
from ..inbound.repo import RepoUnavailable
|
||||||
|
|
||||||
|
_LIST_UNPUSHED_SQL = """
|
||||||
|
SELECT
|
||||||
|
fid, operate_type, oms_id, platform_type, supplier_name, bar_code,
|
||||||
|
purchase_price, purchase_qty, purchase_total, status, fail_reason,
|
||||||
|
pay_order_no, pay_order_dtl_id, org_code, push_status
|
||||||
|
FROM rpa_cart_settlement_goods_data
|
||||||
|
WHERE push_status = 0
|
||||||
|
ORDER BY oms_id, fid
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class OracleCartSettlementRepo:
|
||||||
|
def __init__(self, connect: Callable):
|
||||||
|
self._connect = connect
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_env(cls) -> "OracleCartSettlementRepo":
|
||||||
|
user = os.getenv("RPA_ORACLE_USER", "")
|
||||||
|
password = os.getenv("RPA_ORACLE_PASSWORD", "")
|
||||||
|
host = os.getenv("RPA_ORACLE_HOST", "")
|
||||||
|
if not user or not password or not host:
|
||||||
|
raise RepoUnavailable("未配置 RPA_ORACLE_HOST / RPA_ORACLE_USER / RPA_ORACLE_PASSWORD")
|
||||||
|
port = os.getenv("RPA_ORACLE_PORT", "1521")
|
||||||
|
service = os.getenv("RPA_ORACLE_SERVICE", "rpa")
|
||||||
|
dsn = f"{host}:{port}/{service}"
|
||||||
|
|
||||||
|
def connect():
|
||||||
|
_ensure_client()
|
||||||
|
import oracledb
|
||||||
|
|
||||||
|
return oracledb.connect(user=user, password=password, dsn=dsn)
|
||||||
|
|
||||||
|
return cls(connect=connect)
|
||||||
|
|
||||||
|
def list_unpushed(self) -> list[dict]:
|
||||||
|
conn = self._connect()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(_LIST_UNPUSHED_SQL)
|
||||||
|
cols = [item[0].lower() for item in cur.description]
|
||||||
|
return [dict(zip(cols, row)) for row in cur.fetchall()]
|
||||||
|
except Exception as exc:
|
||||||
|
raise RepoUnavailable(str(exc)) from exc
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"fid": 100001,
|
||||||
|
"operateType": 1,
|
||||||
|
"omsId": 20260928000001,
|
||||||
|
"platformType": 48,
|
||||||
|
"supplierName": "示例供应商",
|
||||||
|
"barCode": "06901234567890",
|
||||||
|
"purchasePrice": "128.5000",
|
||||||
|
"purchaseQty": 10,
|
||||||
|
"purchaseTotal": "1285.0000",
|
||||||
|
"status": 1,
|
||||||
|
"failReason": null,
|
||||||
|
"payOrderNo": null,
|
||||||
|
"payOrderDtlId": null,
|
||||||
|
"orgCode": "901"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fid": 100002,
|
||||||
|
"operateType": 2,
|
||||||
|
"omsId": 20260928000001,
|
||||||
|
"platformType": 66,
|
||||||
|
"supplierName": "示例供应商",
|
||||||
|
"barCode": "06901234567890",
|
||||||
|
"purchasePrice": "128.5000",
|
||||||
|
"purchaseQty": 10,
|
||||||
|
"purchaseTotal": "1285.0000",
|
||||||
|
"status": null,
|
||||||
|
"failReason": null,
|
||||||
|
"payOrderNo": "PAY20260928000001",
|
||||||
|
"payOrderDtlId": "1233",
|
||||||
|
"orgCode": "901"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"fid": 200001,
|
||||||
|
"omsId": 20260928000001,
|
||||||
|
"supplierId": "SUP001",
|
||||||
|
"supplierName": "示例供应商",
|
||||||
|
"contractSignDate": "2026-09-28",
|
||||||
|
"contractStartDate": "2026-10-01",
|
||||||
|
"contractEndDate": "2027-09-30",
|
||||||
|
"paymentTerm": 90,
|
||||||
|
"goodsId": "G000123456789",
|
||||||
|
"goodsName": "阿莫西林胶囊",
|
||||||
|
"regNo": "国药准字H20201234",
|
||||||
|
"spec": "0.25g*24粒",
|
||||||
|
"unit": "盒",
|
||||||
|
"qty": "10.0000",
|
||||||
|
"purchaseTaxRate": "0.13",
|
||||||
|
"unitPrice": "100.0000",
|
||||||
|
"goodsSourceId": "GS001",
|
||||||
|
"purchaserId": "P001",
|
||||||
|
"purchaserName": "张三",
|
||||||
|
"plannerId": "PL001",
|
||||||
|
"plannerName": "李四",
|
||||||
|
"contractEntryStatus": null,
|
||||||
|
"contractNo": null,
|
||||||
|
"orgCode": "901",
|
||||||
|
"customerType": 1,
|
||||||
|
"goodsMdmCode": "QX0100167583",
|
||||||
|
"supplierMdmCode": "DW0100046361"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fid": 200002,
|
||||||
|
"omsId": 20260928000002,
|
||||||
|
"supplierId": "SUP001",
|
||||||
|
"supplierName": "示例供应商",
|
||||||
|
"contractSignDate": "2026-09-28",
|
||||||
|
"contractStartDate": "2026-10-01",
|
||||||
|
"contractEndDate": "2027-09-30",
|
||||||
|
"paymentTerm": 60,
|
||||||
|
"goodsId": "G000987654321",
|
||||||
|
"goodsName": "注射用头孢曲松钠",
|
||||||
|
"regNo": "国药准字H20123456",
|
||||||
|
"spec": "1.0g",
|
||||||
|
"unit": "支",
|
||||||
|
"qty": "5.0000",
|
||||||
|
"purchaseTaxRate": "0.13",
|
||||||
|
"unitPrice": "45.0000",
|
||||||
|
"goodsSourceId": "GS002",
|
||||||
|
"purchaserId": "P001",
|
||||||
|
"purchaserName": "张三",
|
||||||
|
"plannerId": "PL001",
|
||||||
|
"plannerName": "李四",
|
||||||
|
"contractEntryStatus": 0,
|
||||||
|
"contractNo": "HT20260928002",
|
||||||
|
"orgCode": "901",
|
||||||
|
"customerType": 1,
|
||||||
|
"goodsMdmCode": "QX0100167583",
|
||||||
|
"supplierMdmCode": "DW0100046361"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
[project]
|
[project]
|
||||||
name = "rpa-oms"
|
name = "rpa-oms"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "洁达 RPA × 订单中心接口。先做接口 1 收件。"
|
description = "洁达 RPA × 订单中心接口。接口 1、3 收件;接口 2 组装报文。"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi>=0.115.0",
|
"fastapi>=0.115.0",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
"""接口 2:从表 2 取 push_status=0,组装文档报文。不连 Oracle,不发 HTTP。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from app.outbound.cart_settlement import (
|
||||||
|
MAX_BATCH,
|
||||||
|
build_cart_settlement_envelope,
|
||||||
|
load_cart_settlement_envelope,
|
||||||
|
)
|
||||||
|
from app.outbound.memory_repo import MemoryCartSettlementRepo
|
||||||
|
|
||||||
|
|
||||||
|
def _row(**overrides):
|
||||||
|
row = {
|
||||||
|
"fid": 100001,
|
||||||
|
"operate_type": 1,
|
||||||
|
"oms_id": 20260928000001,
|
||||||
|
"platform_type": 48,
|
||||||
|
"supplier_name": "示例供应商",
|
||||||
|
"bar_code": "06901234567890",
|
||||||
|
"purchase_price": Decimal("128.5000"),
|
||||||
|
"purchase_qty": 10,
|
||||||
|
"purchase_total": Decimal("1285.0000"),
|
||||||
|
"status": 1,
|
||||||
|
"fail_reason": None,
|
||||||
|
"pay_order_no": None,
|
||||||
|
"pay_order_dtl_id": None,
|
||||||
|
"org_code": "901",
|
||||||
|
"push_status": 0,
|
||||||
|
"multi_platform_flag": 0,
|
||||||
|
}
|
||||||
|
row.update(overrides)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def test_assembles_unpushed_rows_as_document_envelope():
|
||||||
|
repo = MemoryCartSettlementRepo(
|
||||||
|
[
|
||||||
|
_row(),
|
||||||
|
_row(
|
||||||
|
fid=100002,
|
||||||
|
operate_type=2,
|
||||||
|
platform_type=66,
|
||||||
|
status=None,
|
||||||
|
pay_order_no="PAY20260928000001",
|
||||||
|
pay_order_dtl_id="1233",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
envelope = load_cart_settlement_envelope(repo)
|
||||||
|
assert envelope == {
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"fid": 100001,
|
||||||
|
"operateType": 1,
|
||||||
|
"omsId": 20260928000001,
|
||||||
|
"platformType": 48,
|
||||||
|
"supplierName": "示例供应商",
|
||||||
|
"barCode": "06901234567890",
|
||||||
|
"purchasePrice": "128.5000",
|
||||||
|
"purchaseQty": 10,
|
||||||
|
"purchaseTotal": "1285.0000",
|
||||||
|
"status": 1,
|
||||||
|
"failReason": None,
|
||||||
|
"payOrderNo": None,
|
||||||
|
"payOrderDtlId": None,
|
||||||
|
"orgCode": "901",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fid": 100002,
|
||||||
|
"operateType": 2,
|
||||||
|
"omsId": 20260928000001,
|
||||||
|
"platformType": 66,
|
||||||
|
"supplierName": "示例供应商",
|
||||||
|
"barCode": "06901234567890",
|
||||||
|
"purchasePrice": "128.5000",
|
||||||
|
"purchaseQty": 10,
|
||||||
|
"purchaseTotal": "1285.0000",
|
||||||
|
"status": None,
|
||||||
|
"failReason": None,
|
||||||
|
"payOrderNo": "PAY20260928000001",
|
||||||
|
"payOrderDtlId": "1233",
|
||||||
|
"orgCode": "901",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_skips_already_pushed_rows():
|
||||||
|
repo = MemoryCartSettlementRepo(
|
||||||
|
[
|
||||||
|
_row(fid=1, push_status=1),
|
||||||
|
_row(fid=2, oms_id=20260928000002, push_status=0),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
envelope = load_cart_settlement_envelope(repo)
|
||||||
|
assert [item["fid"] for item in envelope["data"]] == [2]
|
||||||
|
|
||||||
|
|
||||||
|
def test_keeps_same_oms_id_in_one_envelope():
|
||||||
|
rows = [
|
||||||
|
_row(fid=1, oms_id=1, platform_type=48, multi_platform_flag=1),
|
||||||
|
_row(fid=2, oms_id=1, platform_type=66, multi_platform_flag=1),
|
||||||
|
_row(fid=3, oms_id=2, platform_type=48),
|
||||||
|
]
|
||||||
|
envelope = build_cart_settlement_envelope(rows, limit=2)
|
||||||
|
fids = [item["fid"] for item in envelope["data"]]
|
||||||
|
assert fids == [1, 2]
|
||||||
|
|
||||||
|
|
||||||
|
def test_does_not_split_oms_id_group_across_batch_limit():
|
||||||
|
rows = [
|
||||||
|
_row(fid=1, oms_id=1, platform_type=48),
|
||||||
|
_row(fid=2, oms_id=2, platform_type=48, multi_platform_flag=1),
|
||||||
|
_row(fid=3, oms_id=2, platform_type=66, multi_platform_flag=1),
|
||||||
|
]
|
||||||
|
envelope = build_cart_settlement_envelope(rows, limit=2)
|
||||||
|
assert [item["fid"] for item in envelope["data"]] == [1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_batch_cap_is_500():
|
||||||
|
assert MAX_BATCH == 500
|
||||||
|
rows = [_row(fid=i, oms_id=i) for i in range(1, 502)]
|
||||||
|
envelope = build_cart_settlement_envelope(rows)
|
||||||
|
assert len(envelope["data"]) == 500
|
||||||
|
assert envelope["data"][0]["fid"] == 1
|
||||||
|
assert envelope["data"][-1]["fid"] == 500
|
||||||
|
|
||||||
|
|
||||||
|
def test_payload_omits_table_only_columns():
|
||||||
|
envelope = build_cart_settlement_envelope([_row()])
|
||||||
|
item = envelope["data"][0]
|
||||||
|
assert "pushStatus" not in item
|
||||||
|
assert "push_status" not in item
|
||||||
|
assert "multiPlatformFlag" not in item
|
||||||
|
assert "createTime" not in item
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
"""接口 2 Oracle 取数:只捞 push_status=0。不连真库。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from app.outbound.oracle_repo import _LIST_UNPUSHED_SQL, OracleCartSettlementRepo
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_sql_filters_unpushed_and_orders_by_oms_id():
|
||||||
|
sql = " ".join(_LIST_UNPUSHED_SQL.split())
|
||||||
|
assert "rpa_cart_settlement_goods_data" in sql.lower()
|
||||||
|
assert "push_status = 0" in sql
|
||||||
|
assert "order by oms_id" in sql.lower()
|
||||||
|
assert "fid" in sql.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_unpushed_maps_columns():
|
||||||
|
class FakeCursor:
|
||||||
|
description = (
|
||||||
|
("FID",),
|
||||||
|
("OPERATE_TYPE",),
|
||||||
|
("OMS_ID",),
|
||||||
|
("PLATFORM_TYPE",),
|
||||||
|
("SUPPLIER_NAME",),
|
||||||
|
("BAR_CODE",),
|
||||||
|
("PURCHASE_PRICE",),
|
||||||
|
("PURCHASE_QTY",),
|
||||||
|
("PURCHASE_TOTAL",),
|
||||||
|
("STATUS",),
|
||||||
|
("FAIL_REASON",),
|
||||||
|
("PAY_ORDER_NO",),
|
||||||
|
("PAY_ORDER_DTL_ID",),
|
||||||
|
("ORG_CODE",),
|
||||||
|
("PUSH_STATUS",),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def execute(self, sql, params=None):
|
||||||
|
self.sql = sql
|
||||||
|
|
||||||
|
def fetchall(self):
|
||||||
|
return [
|
||||||
|
(
|
||||||
|
100001,
|
||||||
|
1,
|
||||||
|
20260928000001,
|
||||||
|
48,
|
||||||
|
"示例供应商",
|
||||||
|
"06901234567890",
|
||||||
|
128.5,
|
||||||
|
10,
|
||||||
|
1285,
|
||||||
|
1,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"901",
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
class FakeConn:
|
||||||
|
def cursor(self):
|
||||||
|
return FakeCursor()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
repo = OracleCartSettlementRepo(connect=lambda: FakeConn())
|
||||||
|
rows = repo.list_unpushed()
|
||||||
|
assert rows[0]["fid"] == 100001
|
||||||
|
assert rows[0]["operate_type"] == 1
|
||||||
|
assert rows[0]["bar_code"] == "06901234567890"
|
||||||
|
assert rows[0]["push_status"] == 0
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
"""接口 3 Oracle 仓库:INSERT 绑 fid;ORA-00001 映射成 DuplicateFid。不连真库。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.inbound.repo import DuplicateFid, RepoUnavailable
|
||||||
|
|
||||||
|
|
||||||
|
def _record():
|
||||||
|
return {
|
||||||
|
"fid": 200001,
|
||||||
|
"oms_id": 20260928000001,
|
||||||
|
"supplier_id": "SUP001",
|
||||||
|
"supplier_name": "示例供应商",
|
||||||
|
"contract_sign_date": date(2026, 9, 28),
|
||||||
|
"contract_start_date": date(2026, 10, 1),
|
||||||
|
"contract_end_date": date(2027, 9, 30),
|
||||||
|
"payment_term": 90,
|
||||||
|
"goods_id": "G000123456789",
|
||||||
|
"goods_name": "阿莫西林胶囊",
|
||||||
|
"reg_no": "国药准字H20201234",
|
||||||
|
"spec": "0.25g*24粒",
|
||||||
|
"unit": "盒",
|
||||||
|
"qty": Decimal("10.0000"),
|
||||||
|
"purchase_tax_rate": Decimal("0.1300"),
|
||||||
|
"unit_price": Decimal("100.0000"),
|
||||||
|
"goods_source_id": "GS001",
|
||||||
|
"purchaser_id": "P001",
|
||||||
|
"purchaser_name": "张三",
|
||||||
|
"planner_id": "PL001",
|
||||||
|
"planner_name": "李四",
|
||||||
|
"contract_entry_status": None,
|
||||||
|
"contract_no": None,
|
||||||
|
"org_code": "901",
|
||||||
|
"customer_type": 1,
|
||||||
|
"goods_mdm_code": "QX0100167583",
|
||||||
|
"supplier_mdm_code": "DW0100046361",
|
||||||
|
"process_status": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_env_without_host_raises(monkeypatch):
|
||||||
|
monkeypatch.delenv("RPA_ORACLE_HOST", raising=False)
|
||||||
|
from app.inbound.oracle_contract_repo import OraclePurchaseContractRepo
|
||||||
|
|
||||||
|
with pytest.raises(RepoUnavailable):
|
||||||
|
OraclePurchaseContractRepo.from_env()
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_unique_violation_is_duplicate_fid():
|
||||||
|
from app.inbound.oracle_contract_repo import OraclePurchaseContractRepo
|
||||||
|
|
||||||
|
class FakeError(Exception):
|
||||||
|
def __init__(self):
|
||||||
|
self.args = (type("Info", (), {"code": 1})(),)
|
||||||
|
|
||||||
|
class FakeCursor:
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def execute(self, sql, params=None):
|
||||||
|
raise FakeError()
|
||||||
|
|
||||||
|
class FakeConn:
|
||||||
|
def cursor(self):
|
||||||
|
return FakeCursor()
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
raise AssertionError("unique 冲突不应 commit")
|
||||||
|
|
||||||
|
def rollback(self):
|
||||||
|
self.rolled_back = True
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
repo = OraclePurchaseContractRepo(connect=lambda: FakeConn())
|
||||||
|
with pytest.raises(DuplicateFid):
|
||||||
|
repo.insert(_record())
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_sql_binds_source_fid_and_pending_status():
|
||||||
|
from app.inbound.oracle_contract_repo import _BIND_KEYS, _INSERT_SQL
|
||||||
|
|
||||||
|
sql = " ".join(_INSERT_SQL.split())
|
||||||
|
assert "fid" in sql
|
||||||
|
assert ":fid" in sql
|
||||||
|
assert "process_status" in sql
|
||||||
|
assert "SYSTIMESTAMP" in sql
|
||||||
|
assert "fid" in _BIND_KEYS
|
||||||
|
assert "oms_id" in _BIND_KEYS
|
||||||
|
assert "goods_mdm_code" in _BIND_KEYS
|
||||||
|
assert "supplier_mdm_code" in _BIND_KEYS
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
"""接口 3 收件契约。内存仓库,不连 Oracle。按对方 fid 幂等。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
os.environ["OMS_INBOUND_TOKEN"] = "test-oms-token"
|
||||||
|
|
||||||
|
from app.inbound.memory_repo import MemoryPurchaseContractRepo
|
||||||
|
from app.inbound.repo import RepoUnavailable
|
||||||
|
from app.main import app, get_contract_repo
|
||||||
|
|
||||||
|
TOKEN = "test-oms-token"
|
||||||
|
PATH = "/api/oms/v1/purchase-contracts"
|
||||||
|
|
||||||
|
|
||||||
|
def _item(**overrides):
|
||||||
|
row = {
|
||||||
|
"fid": 200001,
|
||||||
|
"omsId": 20260928000001,
|
||||||
|
"supplierId": "SUP001",
|
||||||
|
"supplierName": "示例供应商",
|
||||||
|
"contractSignDate": "2026-09-28",
|
||||||
|
"contractStartDate": "2026-10-01",
|
||||||
|
"contractEndDate": "2027-09-30",
|
||||||
|
"paymentTerm": 90,
|
||||||
|
"goodsId": "G000123456789",
|
||||||
|
"goodsName": "阿莫西林胶囊",
|
||||||
|
"regNo": "国药准字H20201234",
|
||||||
|
"spec": "0.25g*24粒",
|
||||||
|
"unit": "盒",
|
||||||
|
"qty": "10.0000",
|
||||||
|
"purchaseTaxRate": "0.13",
|
||||||
|
"unitPrice": "100.0000",
|
||||||
|
"goodsSourceId": "GS001",
|
||||||
|
"purchaserId": "P001",
|
||||||
|
"purchaserName": "张三",
|
||||||
|
"plannerId": "PL001",
|
||||||
|
"plannerName": "李四",
|
||||||
|
"contractEntryStatus": None,
|
||||||
|
"contractNo": None,
|
||||||
|
"orgCode": "901",
|
||||||
|
"customerType": 1,
|
||||||
|
"goodsMdmCode": "QX0100167583",
|
||||||
|
"supplierMdmCode": "DW0100046361",
|
||||||
|
}
|
||||||
|
row.update(overrides)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client():
|
||||||
|
repo = MemoryPurchaseContractRepo()
|
||||||
|
app.dependency_overrides[get_contract_repo] = lambda: repo
|
||||||
|
with TestClient(app) as test_client:
|
||||||
|
yield test_client, repo
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def _post(client, body, token=TOKEN):
|
||||||
|
headers = {"X-Oms-Token": token} if token is not None else {}
|
||||||
|
return client.post(PATH, json=body, headers=headers)
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_token_returns_401(client):
|
||||||
|
test_client, _ = client
|
||||||
|
resp = _post(test_client, {"data": [_item()]}, token=None)
|
||||||
|
assert resp.status_code == 401
|
||||||
|
assert resp.json()["results"] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_data_returns_422(client):
|
||||||
|
test_client, repo = client
|
||||||
|
resp = _post(test_client, {})
|
||||||
|
assert resp.status_code == 422
|
||||||
|
assert repo.count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_over_100_rows_returns_422(client):
|
||||||
|
test_client, repo = client
|
||||||
|
rows = [_item(fid=i, omsId=i) for i in range(1, 102)]
|
||||||
|
resp = _post(test_client, {"data": rows})
|
||||||
|
assert resp.status_code == 422
|
||||||
|
assert repo.count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_accepts_valid_row(client):
|
||||||
|
test_client, repo = client
|
||||||
|
resp = _post(test_client, {"data": [_item()]})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
assert body["code"] == 200
|
||||||
|
assert body["results"] == [
|
||||||
|
{
|
||||||
|
"fid": 200001,
|
||||||
|
"resultCode": "ACCEPTED",
|
||||||
|
"message": "采购合同数据已接收,等待处理",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
stored = repo.get(200001)
|
||||||
|
assert stored["process_status"] == 0
|
||||||
|
assert stored["oms_id"] == 20260928000001
|
||||||
|
assert stored["supplier_id"] == "SUP001"
|
||||||
|
assert stored["goods_mdm_code"] == "QX0100167583"
|
||||||
|
assert stored["contract_entry_status"] is None
|
||||||
|
assert stored["contract_no"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_existing_fid_is_duplicate_and_not_updated(client):
|
||||||
|
test_client, repo = client
|
||||||
|
_post(test_client, {"data": [_item()]})
|
||||||
|
resp = _post(test_client, {"data": [_item(goodsName="不应覆盖")]})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
result = resp.json()["results"][0]
|
||||||
|
assert result["resultCode"] == "DUPLICATE"
|
||||||
|
assert result["message"] == "序号已存在,未重复入库"
|
||||||
|
assert repo.get(200001)["goods_name"] == "阿莫西林胶囊"
|
||||||
|
|
||||||
|
|
||||||
|
def test_duplicate_fid_in_same_batch_is_conflict(client):
|
||||||
|
test_client, repo = client
|
||||||
|
resp = _post(
|
||||||
|
test_client,
|
||||||
|
{"data": [_item(), _item(omsId=20260928000002)]},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
results = resp.json()["results"]
|
||||||
|
assert results[0]["resultCode"] == "ACCEPTED"
|
||||||
|
assert results[1]["resultCode"] == "CONFLICT"
|
||||||
|
assert results[1]["fid"] == 200001
|
||||||
|
assert repo.count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_retry_protocol_without_contract_no_is_rejected(client):
|
||||||
|
test_client, repo = client
|
||||||
|
resp = _post(test_client, {"data": [_item(contractEntryStatus=0, contractNo=None)]})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
result = resp.json()["results"][0]
|
||||||
|
assert result["resultCode"] == "REJECTED"
|
||||||
|
assert "contractNo" in result["message"]
|
||||||
|
assert repo.count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_retry_protocol_with_contract_no_is_accepted(client):
|
||||||
|
test_client, repo = client
|
||||||
|
resp = _post(
|
||||||
|
test_client,
|
||||||
|
{"data": [_item(fid=200002, contractEntryStatus=0, contractNo="HT20260928002")]},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["results"][0]["resultCode"] == "ACCEPTED"
|
||||||
|
stored = repo.get(200002)
|
||||||
|
assert stored["contract_entry_status"] == 0
|
||||||
|
assert stored["contract_no"] == "HT20260928002"
|
||||||
|
|
||||||
|
|
||||||
|
def test_end_date_before_start_is_rejected(client):
|
||||||
|
test_client, repo = client
|
||||||
|
resp = _post(
|
||||||
|
test_client,
|
||||||
|
{"data": [_item(contractStartDate="2027-09-30", contractEndDate="2026-10-01")]},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["results"][0]["resultCode"] == "REJECTED"
|
||||||
|
assert repo.count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_string_oms_id_is_rejected(client):
|
||||||
|
test_client, repo = client
|
||||||
|
resp = _post(test_client, {"data": [_item(omsId="20260928000001")]})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["results"][0]["resultCode"] == "REJECTED"
|
||||||
|
assert repo.count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_mdm_is_rejected_and_not_stored(client):
|
||||||
|
test_client, repo = client
|
||||||
|
resp = _post(test_client, {"data": [_item(goodsMdmCode="")]})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
result = resp.json()["results"][0]
|
||||||
|
assert result["resultCode"] == "REJECTED"
|
||||||
|
assert "goodsMdmCode" in result["message"]
|
||||||
|
assert repo.count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_mixed_batch_still_http_200(client):
|
||||||
|
test_client, repo = client
|
||||||
|
_post(test_client, {"data": [_item(fid=1, omsId=1)]})
|
||||||
|
resp = _post(
|
||||||
|
test_client,
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
_item(fid=1, omsId=1),
|
||||||
|
_item(fid=2, omsId=2),
|
||||||
|
_item(fid=3, omsId=3, orgCode=""),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
codes = [r["resultCode"] for r in resp.json()["results"]]
|
||||||
|
assert codes == ["DUPLICATE", "ACCEPTED", "REJECTED"]
|
||||||
|
assert repo.count() == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_repo_unavailable_returns_503(client):
|
||||||
|
test_client, _ = client
|
||||||
|
|
||||||
|
class Boom:
|
||||||
|
def insert(self, record):
|
||||||
|
raise RepoUnavailable("数据库不可用")
|
||||||
|
|
||||||
|
app.dependency_overrides[get_contract_repo] = lambda: Boom()
|
||||||
|
resp = _post(test_client, {"data": [_item()]})
|
||||||
|
assert resp.status_code == 503
|
||||||
|
assert resp.json()["code"] == 503
|
||||||
|
assert resp.json()["results"] == []
|
||||||
Loading…
Reference in New Issue