Metadata-Version: 2.4
Name: dwy-wechatpay-notify
Version: 0.1.0
Summary: 微信支付官方回调验证与安全通知事件适配
License: MIT License
        
        Copyright (c) 2026 DWY
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Requires-Python: <3.13,>=3.11
Requires-Dist: dwy-notify<0.4,>=0.2.1
Requires-Dist: pydantic<3,>=2.10
Requires-Dist: wechatpayv3[async]==2.0.4
Provides-Extra: dev
Requires-Dist: httpx==0.28.1; extra == 'dev'
Requires-Dist: pytest-asyncio==1.3.0; extra == 'dev'
Requires-Dist: pytest==9.0.3; extra == 'dev'
Requires-Dist: ruff==0.15.12; extra == 'dev'
Provides-Extra: fastapi
Requires-Dist: fastapi<1,>=0.115; extra == 'fastapi'
Description-Content-Type: text/markdown

# dwy-wechatpay-notify

独立微信支付来源包（0.1.0）：复用 `wechatpayv3[async]==2.0.4` 的公开 `callback(headers, body)` 验签解密，把付款和退款结果转成 `dwy_notify.events.NotificationEvent`。不依赖宿主支付模块，不实现 RSA/AES、业务账本、权益变更或群消息发送。

本包采用 [MIT License](LICENSE)。微信支付 SDK `wechatpayv3==2.0.4` 同样采用 MIT，具体版权声明以各发行包为准。

## 已有支付后台

已有官方回调入口的项目直接使用 verifier，不重复注册本包路由。SDK 客户端、商户私钥、APIv3 密钥、公钥/平台证书全部由宿主配置；在 SDK 客户端有效上下文内注入它的同步公开 callback。本包不读取环境变量、不复制密钥、不调用 SDK 私有 core。

```python
from dwy_wechatpay_notify import (
    PaymentNotificationContext,
    WechatPayNotificationVerifier,
    WechatPayNotifyConfig,
    wechatpay_notification_event,
)


async def handle_callback(headers, raw_body, sdk_client, payment_service, durable_sink):
    """由宿主传入已打开的 SDK 客户端、原始 bytes 和持久接收器。"""
    config = WechatPayNotifyConfig(mchid="1900000001", appid="wx123", scope="shop-production")
    verified = await WechatPayNotificationVerifier(config, callback=sdk_client.callback).verify(headers, raw_body)
    # 宿主核对本地订单/金额并完成业务提交；持久 outbox 应与业务事务一起写入。
    confirmed = await payment_service.apply_verified_notification(verified)
    event = wechatpay_notification_event(
        confirmed,
        scope=config.scope,
        context=PaymentNotificationContext(description="基础版月付", outcome="权益已确认"),
    )
    await durable_sink(event)
```

`payment_service` 与 `durable_sink` 是宿主注入对象，示例不定义业务账本。主动查单、补单或对账时构造 `ConfirmedWechatPayResult`：付款需 `event_type="TRANSACTION.SUCCESS"`、`order_no`、`transaction_id`、整数分 `amount_fen`、`trade_type`、`trade_state="SUCCESS"`；退款另需 `refund_no`、`refund_id`、`refund_fen`、与事件一致的 `refund_status`。查单没有官方回调 ID 时保持 `provider_event_id=None`。

收到已验证官方事件**不证明本地已入账**。宿主必须核对订单、币种、原金额与状态，并由自己的事务确定权益。`REFUND.ABNORMAL`、`REFUND.CLOSED` 只表示异常/关闭，不能当作退款成功。退款回调可能没有 appid，始终核验 mchid；有 appid 时同时核验。

## 新项目：可选 FastAPI 安装器

安装包的 `[fastapi]` extra 后调用以下装配函数；生产必须传入持久化 `EventSink`，不得用只发 HTTP 群消息的函数替代持久接受。`sink(event)` 返回 True 表示新接受，False 表示之前已接受的重复，二者均确认；异常返回 HTTP 503，微信可重试。本模块不在验签前用内存 receipt 吞掉业务重试。

```python
from dwy_wechatpay_notify import (
    WechatPayNotificationVerifier,
    WechatPayNotifyConfig,
    install_wechatpay_notifications,
)


def configure_callbacks(app, sdk_client, durable_sink):
    """宿主 lifespan 保持 SDK 客户端存活，关闭资源仍由宿主负责。"""
    config = WechatPayNotifyConfig(mchid="1900000001", appid="wx123", scope="shop-production")
    install_wechatpay_notifications(
        app,
        config,
        verifier=WechatPayNotificationVerifier(config, callback=sdk_client.callback),
        sink=durable_sink,
    )
```

默认接收地址示例：`http://localhost:8000/webhooks/wechatpay`；生产应配置真实公网 HTTPS，例如 `https://payments.example.com:443/webhooks/wechatpay`。可通过 config.path 更改。安装器流式读取请求，默认最多 65536 bytes；超限 HTTP 413，验签/身份/协议失败 HTTP 400，持久接受成功或重复 HTTP 204。只交给 SDK 一次原始 bytes，不改变宿主 lifespan、不启动或关闭其他应用。

## 安全与幂等

- 配置显式注入，商户/应用身份非空，scope 最长 128 字符；不同商户/应用必须使用不同 scope。
- 付款键 `wechat:payment:{order_no}:success`；退款键 `wechat:refund:{refund_no}:{SUCCESS|ABNORMAL|CLOSED}`。回调与主动查单使用同一业务键；sink 按 source、scope、event_id 持久幂等。
- 事件 data 仅包含类型化白名单字段；不转发 payer/openid、邮箱、手机号、密钥、签名或原文。`PaymentNotificationContext` 仅接安全商品快照/已确认业务结果，不接受任意字典；调用方不能在这些文字里填个人信息或凭据。
- 通知正文严格不超过 1800 UTF8 bytes；放不下的商品/业务结果整项省略，不截断金融标识。金额用整数分格式化。
- `WECHATPAY/SIGNTEST/` 不是交易，安全拒绝；SDK 的异常统一转成 `WechatPayVerificationError`，不携带原文或密钥。宿主不得给 SDK 配置记录回调原文的日志器。

## 开发与验收

执行本目录 `pytest` 运行全部用例，使用固定测试驱动与本地临时 RSA 密钥/AESGCM，均不访问微信/企微网络。`tests/TEST_CASES.md` 记录覆盖边界。`tests/wheel_smoke.py` 面向已安装 wheel，验证独立核心导入及事件合同。正式发行必须使用 GitHub Actions OIDC；本地构建仅做临时 wheel 验收，不替代发布工作流。
