diff --git a/.env.example b/.env.example index 7769110..c833384 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,25 @@ # 获取方式:微信公众平台 mp.weixin.qq.com → 开发 → 开发管理 → 开发设置 → AppID / AppSecret WX_APPID=你的小程序AppID WX_SECRET=你的小程序AppSecret + +# ── 微信支付 V3(算力充值,app/pay 子应用)──────────────────────────── +# 商户平台 pay.weixin.qq.com 获取;凭据只放部署环境 .env,勿提交 +PINEAGENTS_WECHATPAY_MCHID= +PINEAGENTS_WECHATPAY_CERT_SERIAL_NO= +PINEAGENTS_WECHATPAY_APIV3_KEY= +# 商户 API 证书私钥路径(默认 serverdata/keys/wechatpay/apiclient_key.pem) +PINEAGENTS_WECHATPAY_PRIVATE_KEY_PATH= +# 平台公钥(可选,公钥模式时配置);否则平台证书自动更新落盘 +PINEAGENTS_WECHATPAY_PUBLIC_KEY_PATH= +# 支付回调地址:须为微信可达的公网 HTTPS +PINEAGENTS_WECHATPAY_NOTIFY_URL=https://opc.pinesound.cn/opc/pay/notify +# Native 扫码用 appid(公众号/开放平台,须绑定到该商户号);空回退开放平台/小程序 appid +PINEAGENTS_WECHATPAY_NATIVE_APPID= +# 小程序 JSAPI 用 appid;空回退 PINEAGENTS_WECHAT_APPID +PINEAGENTS_WECHATPAY_JSAPI_APPID= +# 充值套餐兜底 JSON([{id,amount(元),bonus(赠送元),label}]);DB system_configs 的 recharge_packages 优先 +PINEAGENTS_WECHATPAY_PACKAGES=[{"id":"p10","amount":10,"bonus":0,"label":"10 元"},{"id":"p50","amount":50,"bonus":5,"label":"50 元"},{"id":"p100","amount":100,"bonus":15,"label":"100 元"}] +# 自由金额范围(元)与订单有效期(分钟) +PINEAGENTS_RECHARGE_MIN_YUAN=1 +PINEAGENTS_RECHARGE_MAX_YUAN=5000 +PINEAGENTS_RECHARGE_EXPIRE_MINUTES=15 diff --git a/alembic/env.py b/alembic/env.py index 5ec481c..4e4bce9 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -20,6 +20,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from app import config as app_config from app.infrastructure.db import Base import app.infrastructure.models # noqa: F401 确保模型注册到 Base.metadata +import app.pay.models # noqa: F401 支付子应用模型(充值订单)注册到同一 metadata config = context.config if config.config_file_name is not None: diff --git a/alembic/versions/0026_compute_recharge.py b/alembic/versions/0026_compute_recharge.py new file mode 100644 index 0000000..2ad6c78 --- /dev/null +++ b/alembic/versions/0026_compute_recharge.py @@ -0,0 +1,46 @@ +"""算力充值订单表(app/pay 支付子应用,微信支付 V3) + +Revision ID: 0026_compute_recharge +Revises: 0025_comment_reply_like +Create Date: 2026-08-30 +""" +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + +revision = "0026_compute_recharge" +down_revision = "0025_comment_reply_like" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "compute_recharge_orders", + sa.Column("id", sa.String(), primary_key=True), + sa.Column("order_no", sa.String(), nullable=False, unique=True, index=True), + sa.Column("user_id", sa.String(), nullable=False, server_default="", index=True), + sa.Column("username", sa.String(), nullable=False, server_default="", index=True), + sa.Column("engine_user_id", sa.Integer(), nullable=False, server_default="0"), + sa.Column("package_id", sa.String(), nullable=False, server_default=""), + sa.Column("amount_fen", sa.Integer(), nullable=False, server_default="0"), + sa.Column("quota_micro", sa.Integer(), nullable=False, server_default="0"), + sa.Column("bonus_quota_micro", sa.Integer(), nullable=False, server_default="0"), + sa.Column("client_type", sa.String(), nullable=False, server_default="native"), + sa.Column("appid", sa.String(), nullable=False, server_default=""), + sa.Column("status", sa.String(), nullable=False, server_default="pending", index=True), + sa.Column("prepay_id", sa.String(), nullable=False, server_default=""), + sa.Column("transaction_id", sa.String(), nullable=False, server_default="", index=True), + sa.Column("code_url", sa.Text(), nullable=False, server_default=""), + sa.Column("expires_at", sa.String(), nullable=False, server_default=""), + sa.Column("paid_at", sa.String(), nullable=False, server_default=""), + sa.Column("credited_at", sa.String(), nullable=False, server_default=""), + sa.Column("notify_payload", sa.Text(), nullable=False, server_default=""), + sa.Column("created_at", sa.String(), nullable=False, server_default="", index=True), + sa.Column("updated_at", sa.String(), nullable=False, server_default=""), + ) + + +def downgrade() -> None: + op.drop_table("compute_recharge_orders") diff --git a/app/infrastructure/repositories.py b/app/infrastructure/repositories.py index 0bcc9c4..441668e 100644 --- a/app/infrastructure/repositories.py +++ b/app/infrastructure/repositories.py @@ -2085,7 +2085,7 @@ class ServiceReferralRepository: stmt = select(ServiceReferral).order_by(ServiceReferral.created_at.desc()) if carrier_id: stmt = stmt.where(ServiceReferral.carrier_id == carrier_id) - return [await self._to_dict(r) for r in await self.session.scalars(stmt)] + return [self._to_dict(r) for r in await self.session.scalars(stmt)] async def create(self, fields: dict) -> dict: row = ServiceReferral( @@ -2139,7 +2139,7 @@ class EscrowRepository: stmt = select(Escrow).order_by(Escrow.created_at.desc()) if status: stmt = stmt.where(Escrow.status == status) - return [await self._to_dict(r) for r in await self.session.scalars(stmt)] + return [self._to_dict(r) for r in await self.session.scalars(stmt)] async def get(self, escrow_id: str) -> dict | None: row = await self.session.get(Escrow, escrow_id) @@ -2200,7 +2200,7 @@ class DisputeRepository: stmt = select(Dispute).order_by(Dispute.created_at.desc()) if status: stmt = stmt.where(Dispute.status == status) - return [await self._to_dict(r) for r in await self.session.scalars(stmt)] + return [self._to_dict(r) for r in await self.session.scalars(stmt)] async def create(self, task_id: str, task_title: str, initiator: str, reason: str) -> dict: row = Dispute(id=new_id("disp"), task_id=task_id, task_title=task_title, @@ -2429,6 +2429,10 @@ class Database: self.notifications = NotificationRepository(self.session) self.org_members = OrganizationMemberRepository(self.session) self.stats = StatsRepository(self.session) + # 支付子应用订单仓储(延迟导入,避免基础设施层与 app/pay 的模块级循环依赖) + from ..pay.repository import ComputeRechargeOrderRepository + + self.compute_recharges = ComputeRechargeOrderRepository(self.session) async def initialize(self) -> None: """运行时仅保活:不建表、不灌种子(迁移/种子一律走 alembic + scripts/db/seed.py)。 diff --git a/app/main.py b/app/main.py index 903cd29..5e0e0f9 100644 --- a/app/main.py +++ b/app/main.py @@ -25,6 +25,7 @@ from app.api.routers import rbac_ecosystem as rbac_ecosystem_router from app.api.routers import rbac_portals as rbac_portals_router from app.api.routers import templates as templates_router from app.api.routers import relay as relay_router +from app.pay import routers as pay_router APP_NAME = "云超服Agents 演示后端" @@ -82,6 +83,7 @@ app.include_router(rbac_org_router.router) app.include_router(rbac_portals_router.router) app.include_router(rbac_ecosystem_router.router) app.include_router(relay_router.router) +app.include_router(pay_router.router) @app.get("/health", tags=["meta"], summary="健康检查") diff --git a/app/pay/__init__.py b/app/pay/__init__.py new file mode 100644 index 0000000..bf05d27 --- /dev/null +++ b/app/pay/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +"""支付子应用(app/pay):微信支付 V3 + 算力充值。 + +独立子应用,便于后期整体拆分为支付微服务: +- ``config.py`` 商户配置与充值规则 +- ``models.py`` 充值订单 ORM(注册进平台 Base.metadata,落主库,alembic 统一迁移) +- ``repository.py`` 订单仓储(行级操作) +- ``wxpay.py`` AsyncWeChatPay 客户端封装(懒加载 / 验签解密 / 下单 / 查单) +- ``service.py`` 充值业务(下单 / 回调到账 / 对账 / 幂等) +- ``routers.py`` 接口层(/opc/pay/notify 回调 + /opc/compute/recharge/*) +""" diff --git a/app/pay/config.py b/app/pay/config.py new file mode 100644 index 0000000..1bad99e --- /dev/null +++ b/app/pay/config.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +"""支付子应用配置(微信支付 V3 / 算力充值)。 + +独立成模块便于后期整体拆分为支付微服务;凭据只放服务器 .env,勿提交。 +""" +from __future__ import annotations + +import json +import os + +from .. import config + +# --------------------------------------------------------------------------- +# 微信支付 V3 商户配置(未配置时支付功能整体降级不可用,接口返回 enabled=False) +# --------------------------------------------------------------------------- +WECHATPAY_MCHID = os.environ.get("PINEAGENTS_WECHATPAY_MCHID", "") +WECHATPAY_CERT_SERIAL_NO = os.environ.get("PINEAGENTS_WECHATPAY_CERT_SERIAL_NO", "") +WECHATPAY_APIV3_KEY = os.environ.get("PINEAGENTS_WECHATPAY_APIV3_KEY", "") +WECHATPAY_PRIVATE_KEY_PATH = os.environ.get( + "PINEAGENTS_WECHATPAY_PRIVATE_KEY_PATH", str(config.KEYS_DIR / "wechatpay" / "apiclient_key.pem"), +) +# 平台证书目录(微信支付平台证书/公钥自动更新落盘,属运行时数据) +WECHATPAY_CERT_DIR = os.environ.get( + "PINEAGENTS_WECHATPAY_CERT_DIR", str(config.KEYS_DIR / "wechatpay" / "certs"), +) +# 回调通知地址:须为微信可达的公网 HTTPS(如 https://opc.pinesound.cn/opc/pay/notify) +WECHATPAY_NOTIFY_URL = os.environ.get("PINEAGENTS_WECHATPAY_NOTIFY_URL", "") +# 微信支付平台公钥(可选:新商户用公钥模式时配置;否则走平台证书自动更新) +WECHATPAY_PUBLIC_KEY_PATH = os.environ.get("PINEAGENTS_WECHATPAY_PUBLIC_KEY_PATH", "") +# 平台公钥 ID(公钥模式时与上面成对配置,如 PUB_KEY_ID_0111...) +WECHATPAY_PUBLIC_KEY_ID = os.environ.get("PINEAGENTS_WECHATPAY_PUBLIC_KEY_ID", "") + +# 双 appid:Native 扫码用「绑定到商户号的公众号/开放平台 appid」; +# JSAPI(小程序) 下单 appid 必须是小程序 appid(wechatpayv3 pay() 支持按单覆盖)。 +WECHATPAY_NATIVE_APPID = ( + os.environ.get("PINEAGENTS_WECHATPAY_NATIVE_APPID", "") or config.WECHAT_OPEN_APPID +) +WECHATPAY_JSAPI_APPID = ( + os.environ.get("PINEAGENTS_WECHATPAY_JSAPI_APPID", "") or config.WECHAT_APPID +) + +# --------------------------------------------------------------------------- +# 充值规则 +# --------------------------------------------------------------------------- +# 人民币实际计费口径:1 元 = 1,000,000 微元(与引擎 meter 的 QUOTA_PER_YUAN 一致) +QUOTA_PER_YUAN = 1_000_000 + +RECHARGE_MIN_YUAN = float(os.environ.get("PINEAGENTS_RECHARGE_MIN_YUAN", "1")) +RECHARGE_MAX_YUAN = float(os.environ.get("PINEAGENTS_RECHARGE_MAX_YUAN", "5000")) +RECHARGE_EXPIRE_MINUTES = int(os.environ.get("PINEAGENTS_RECHARGE_EXPIRE_MINUTES", "15")) + +# 充值套餐兜底(JSON 数组字符串):DB system_configs 的 recharge_packages 优先 +DEFAULT_PACKAGES_JSON = os.environ.get( + "PINEAGENTS_WECHATPAY_PACKAGES", + json.dumps([ + {"id": "p10", "amount": 10, "bonus": 0, "label": "10 元"}, + {"id": "p50", "amount": 50, "bonus": 5, "label": "50 元"}, + {"id": "p100", "amount": 100, "bonus": 15, "label": "100 元"}, + ], ensure_ascii=False), +) + + +def pay_enabled() -> bool: + """商户关键凭据齐备才启用支付。""" + return bool( + WECHATPAY_MCHID and WECHATPAY_CERT_SERIAL_NO and WECHATPAY_APIV3_KEY + and WECHATPAY_PRIVATE_KEY_PATH and WECHATPAY_NOTIFY_URL + ) diff --git a/app/pay/models.py b/app/pay/models.py new file mode 100644 index 0000000..574526b --- /dev/null +++ b/app/pay/models.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +"""支付子应用 ORM:算力充值订单(落平台主库,alembic 统一迁移)。""" +from __future__ import annotations + +from sqlalchemy import Integer, String, Text +from sqlalchemy.orm import Mapped, mapped_column + +from ..infrastructure.db import Base + + +class ComputeRechargeOrder(Base): + """算力充值订单(微信支付 V3)。 + + 状态机:pending → paid(支付成功,本地终态)→ credited(引擎到账完成); + pending 超时未支付 → closed;到账异常 → failed(待人工/对账补单)。 + """ + __tablename__ = "compute_recharge_orders" + + id: Mapped[str] = mapped_column(String, primary_key=True) # cr_ + order_no: Mapped[str] = mapped_column(String, unique=True, index=True) # CR__,微信 out_trade_no + user_id: Mapped[str] = mapped_column(String, default="", index=True) # 平台 users.id + username: Mapped[str] = mapped_column(String, default="", index=True) # 平台/引擎用户名(充值归集键) + engine_user_id: Mapped[int] = mapped_column(Integer, default=0) # 引擎用户 id(下单时解析缓存,0=待解析) + package_id: Mapped[str] = mapped_column(String, default="") # 套餐 id;空 = 自由金额 + amount_fen: Mapped[int] = mapped_column(Integer, default=0) # 实付金额(分,微信口径) + quota_micro: Mapped[int] = mapped_column(Integer, default=0) # 到账微元 = amount_fen//100 * 1_000_000 + bonus_quota_micro: Mapped[int] = mapped_column(Integer, default=0) # 套餐赠送微元 + client_type: Mapped[str] = mapped_column(String, default="native") # native(扫码) | jsapi(小程序) + appid: Mapped[str] = mapped_column(String, default="") # 下单所用 appid(对账依据) + status: Mapped[str] = mapped_column(String, default="pending", index=True) # pending|paid|credited|closed|failed + prepay_id: Mapped[str] = mapped_column(String, default="") # 微信预支付单号(jsapi) + transaction_id: Mapped[str] = mapped_column(String, default="", index=True) # 微信支付单号 + code_url: Mapped[str] = mapped_column(String, default="") # Native 二维码内容 + expires_at: Mapped[str] = mapped_column(String, default="") # 过期时刻(ISO,超时未付 → closed) + paid_at: Mapped[str] = mapped_column(String, default="") + credited_at: Mapped[str] = mapped_column(String, default="") # 引擎到账完成时刻 + notify_payload: Mapped[str] = mapped_column(Text, default="") # 回调/查单原始报文(JSON 串,排查用) + created_at: Mapped[str] = mapped_column(String, default="", index=True) + updated_at: Mapped[str] = mapped_column(String, default="") diff --git a/app/pay/repository.py b/app/pay/repository.py new file mode 100644 index 0000000..5a21a7c --- /dev/null +++ b/app/pay/repository.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +"""支付子应用仓储:充值订单行级操作(不掺 HTTP 逻辑)。""" +from __future__ import annotations + +from sqlalchemy import select + +from ..infrastructure.repositories import new_id, utcnow_iso +from .models import ComputeRechargeOrder + + +def _to_dict(o: ComputeRechargeOrder) -> dict: + return { + "id": o.id, "order_no": o.order_no, "user_id": o.user_id, "username": o.username, + "engine_user_id": o.engine_user_id, "package_id": o.package_id, + "amount_fen": o.amount_fen, "quota_micro": o.quota_micro, + "bonus_quota_micro": o.bonus_quota_micro, "client_type": o.client_type, + "appid": o.appid, "status": o.status, "prepay_id": o.prepay_id, + "transaction_id": o.transaction_id, "code_url": o.code_url, + "expires_at": o.expires_at, "paid_at": o.paid_at, "credited_at": o.credited_at, + "notify_payload": o.notify_payload, "created_at": o.created_at, "updated_at": o.updated_at, + } + + +class ComputeRechargeOrderRepository: + def __init__(self, session): + self.session = session + + async def create(self, fields: dict) -> dict: + now = utcnow_iso() + row = ComputeRechargeOrder( + id=new_id("cr"), created_at=now, updated_at=now, + **{k: v for k, v in fields.items() if hasattr(ComputeRechargeOrder, k) and k != "id"}, + ) + self.session.add(row) + await self.session.commit() + return _to_dict(row) + + async def get_by_order_no(self, order_no: str) -> dict | None: + row = await self.session.scalar( + select(ComputeRechargeOrder).where(ComputeRechargeOrder.order_no == order_no) + ) + return _to_dict(row) if row else None + + async def find_pending_same_amount(self, user_id: str, amount_fen: int, now_iso: str) -> dict | None: + """同用户同金额未过期 pending 单(下单防重复扫码复用)。""" + row = await self.session.scalar( + select(ComputeRechargeOrder) + .where( + ComputeRechargeOrder.user_id == user_id, + ComputeRechargeOrder.amount_fen == amount_fen, + ComputeRechargeOrder.status == "pending", + ComputeRechargeOrder.expires_at > now_iso, + ) + .order_by(ComputeRechargeOrder.created_at.desc()) + .limit(1) + ) + return _to_dict(row) if row else None + + async def list_by_user(self, user_id: str, limit: int = 50) -> list[dict]: + rows = (await self.session.scalars( + select(ComputeRechargeOrder) + .where(ComputeRechargeOrder.user_id == user_id) + .order_by(ComputeRechargeOrder.created_at.desc()) + .limit(limit) + )).all() + return [_to_dict(r) for r in rows] + + async def _row(self, order_no: str) -> ComputeRechargeOrder | None: + return await self.session.scalar( + select(ComputeRechargeOrder).where(ComputeRechargeOrder.order_no == order_no) + ) + + async def mark_paid(self, order_no: str, transaction_id: str, payload_json: str) -> bool: + """pending → paid(幂等排他):仅当仍是 pending 才置 paid;False=已被处理(回调重放/并发)。""" + row = await self._row(order_no) + if row is None or row.status != "pending": + return False + row.status = "paid" + row.transaction_id = transaction_id or row.transaction_id + row.paid_at = utcnow_iso() + row.notify_payload = payload_json + row.updated_at = utcnow_iso() + await self.session.commit() + return True + + async def mark_credited(self, order_no: str) -> None: + """paid → credited:引擎到账完成。""" + row = await self._row(order_no) + if row is None: + return + row.status = "credited" + row.credited_at = utcnow_iso() + row.updated_at = utcnow_iso() + await self.session.commit() + + async def set_prepay(self, order_no: str, appid: str, prepay_id: str) -> None: + """记录 JSAPI 预支付信息(pending 态,小程序扫码拉起支付时回填)。""" + row = await self._row(order_no) + if row is None: + return + row.appid = appid or row.appid + row.prepay_id = prepay_id or row.prepay_id + row.updated_at = utcnow_iso() + await self.session.commit() + + async def mark_status(self, order_no: str, status: str, payload_json: str = "") -> None: + """置 closed / failed 等非终态迁移。""" + row = await self._row(order_no) + if row is None: + return + row.status = status + if payload_json: + row.notify_payload = payload_json + row.updated_at = utcnow_iso() + await self.session.commit() diff --git a/app/pay/routers.py b/app/pay/routers.py new file mode 100644 index 0000000..635ccb6 --- /dev/null +++ b/app/pay/routers.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +"""支付子应用接口层:微信回调 + 算力充值(C 端自服务)。 + +- ``POST /opc/pay/notify`` 微信支付回调(公网、无登录鉴权,靠 V3 验签)。 +- ``/opc/compute/recharge/*`` 用户充值(套餐/下单/状态/记录),require_roles("opc_member")。 +""" +from __future__ import annotations + +import json +import logging + +from fastapi import APIRouter, Depends, HTTPException, Request, Response +from pydantic import BaseModel, Field + +from ..api.dependencies import get_db +from ..infrastructure.repositories import Database +from ..rbac import require_roles +from . import service, wxpay +from .repository import ComputeRechargeOrderRepository + +logger = logging.getLogger("pay.api") + +router = APIRouter(tags=["pay"]) + + +def _repo(db: Database) -> ComputeRechargeOrderRepository: + """订单仓储(复用请求级 session;支付域自持仓储,便于整域拆分微服务)。""" + return ComputeRechargeOrderRepository(db.session) + + +# ── 微信支付回调(微信服务器调用,验签解密后到账)───────────────────────────── +@router.post("/opc/pay/notify", summary="微信支付回调(无登录鉴权,V3 验签)") +async def pay_notify(request: Request, db: Database = Depends(get_db)): + body = await request.body() + result = await wxpay.verify_and_decrypt(request.headers, body) + if not result or not isinstance(result, dict): + raise HTTPException(status_code=400, detail="回调验签/解密失败") + # 展平 resource 到顶层(out_trade_no / transaction_id / trade_state ...) + resource = result.pop("resource", {}) + if isinstance(resource, dict): + result.update(resource) + order_no = result.get("out_trade_no", "") + logger.info("微信支付回调: order=%s trade_state=%s", order_no, result.get("trade_state")) + try: + ok = await service.handle_notify(db, result) + except Exception as exc: # noqa: BLE001 + logger.error("回调处理异常 order=%s: %s", order_no, exc, exc_info=True) + raise HTTPException(status_code=500, detail="处理失败,等待微信重试") from exc + if not ok: + raise HTTPException(status_code=500, detail="处理失败,等待微信重试") + return {"code": "SUCCESS", "message": "成功"} + + +# ── 用户充值(C 端自服务)───────────────────────────────────────────────────── +class RechargeOrderRequest(BaseModel): + """充值下单请求:package_id(套餐)或 amount_yuan(自由金额)二选一。""" + client_type: str = Field(default="native", description="native(桌面扫码) | jsapi(小程序)") + package_id: str = "" + amount_yuan: float | None = Field(default=None, ge=0) + + +@router.get("/opc/compute/recharge/packages", summary="充值套餐与支付开关") +async def recharge_packages( + db: Database = Depends(get_db), + _u: dict = Depends(require_roles("opc_member")), +): + return await service.packages(db) + + +@router.post("/opc/compute/recharge/orders", summary="创建充值订单(native=二维码 / jsapi=小程序支付参数)") +async def recharge_create( + body: RechargeOrderRequest, + db: Database = Depends(get_db), + user: dict = Depends(require_roles("opc_member")), +): + try: + order = await service.create_order( + db, user, client_type=body.client_type, + package_id=body.package_id, amount_yuan=body.amount_yuan, + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except RuntimeError as exc: + raise HTTPException(status_code=503, detail=str(exc)) from exc + return order + + +@router.get("/opc/compute/recharge/orders/{order_no}/pay-params", summary="小程序扫码确认支付(归属校验 + JSAPI 参数)") +async def recharge_pay_params( + order_no: str, + db: Database = Depends(get_db), + user: dict = Depends(require_roles("opc_member")), +): + """微信扫桌面端小程序码 → 打开小程序本接口取 wx.requestPayment 参数。""" + try: + return await service.build_pay_params(db, user, order_no) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except RuntimeError as exc: + raise HTTPException(status_code=503, detail=str(exc)) from exc + + +@router.post("/opc/compute/recharge/orders/{order_no}/status", summary="查询充值订单状态(含主动对账补单)") +async def recharge_status( + order_no: str, + db: Database = Depends(get_db), + user: dict = Depends(require_roles("opc_member")), +): + repo = _repo(db) + order = await repo.get_by_order_no(order_no) + if order is None or order["user_id"] != user["id"]: + raise HTTPException(status_code=404, detail="订单不存在") + order = await service.reconcile(db, order) + return { + "order_no": order["order_no"], "status": order["status"], + "paid": order["status"] in ("paid", "credited"), + "credited": order["status"] == "credited", + "amount_fen": order["amount_fen"], "quota_micro": order["quota_micro"], + "bonus_quota_micro": order["bonus_quota_micro"], + "transaction_id": order["transaction_id"], "expires_at": order["expires_at"], + "paid_at": order.get("paid_at", ""), "credited_at": order.get("credited_at", ""), + } + + +@router.get("/opc/compute/recharge/orders", summary="我的充值记录") +async def recharge_orders( + db: Database = Depends(get_db), + user: dict = Depends(require_roles("opc_member")), +): + return {"items": await _repo(db).list_by_user(user["id"])} diff --git a/app/pay/service.py b/app/pay/service.py new file mode 100644 index 0000000..0d45667 --- /dev/null +++ b/app/pay/service.py @@ -0,0 +1,329 @@ +# -*- coding: utf-8 -*- +"""充值业务服务:下单 / 回调到账 / 对账补单 / 幂等。 + +到账链路(与全平台算力统一口径一致): + 支付成功 → compute_engine users.quota 增加(1 元 = 1,000,000 微元)→ 平台镜像回写。 +幂等三重保证:Redis SETNX 锁(30s)+ ``mark_paid`` 单向状态机 + 订单终态判定。 +事务边界:引擎到账是跨服务 HTTP,无法与本地 DB 同事务 → 先本地置 paid 终态, +再引擎加款;引擎失败置 failed,由 status 对账接口补单(不重复加款)。 +""" +from __future__ import annotations + +import json +import logging +import secrets +import time +from datetime import datetime, timedelta, timezone + +from .. import config +from ..infrastructure.repositories import Database, utcnow_iso +from ..services import compute_client +from . import config as pay_config +from . import wxpay + +logger = logging.getLogger("pay.service") + +# 充值订单号前缀(回调按前缀分发,为未来其他支付业务留扩展位) +ORDER_PREFIX = "CR_" + +# 套餐缓存(60s:DB 配置优先,env 兜底) +_pkg_cache: tuple[float, list[dict]] | None = None +_PKG_TTL = 60.0 + + +def _iso_in(seconds: int) -> str: + return datetime.fromtimestamp( + datetime.now(timezone.utc).timestamp() + seconds, tz=timezone.utc, + ).isoformat() + + +# --------------------------------------------------------------------------- +# 套餐 +# --------------------------------------------------------------------------- +def _normalize_packages(raw) -> list[dict]: + """套餐数据清洗:[{id,amount(元),bonus(元),discount(%),enabled,label}],金额全整数分口径。 + + - discount:充值折扣(默认 100=无折扣),实付 = amount * discount / 100 + - enabled:false 时不下发(运营端可临时下架) + """ + items: list[dict] = [] + if not isinstance(raw, list): + return items + for it in raw: + if isinstance(it, dict) and it.get("enabled") is False: + continue + try: + amount = int(round(float(it.get("amount", 0)) * 100)) + bonus = int(round(float(it.get("bonus", 0) or 0) * 100)) + discount = int(it.get("discount", 100) or 100) + except (TypeError, ValueError): + continue + if amount <= 0: + continue + discount = max(1, min(100, discount)) + items.append({ + "id": str(it.get("id") or f"p{amount}"), + "amount": amount, # 分(原价) + "bonus": bonus, # 分(赠送折算金额) + "discount": discount, # %(100=原价) + "pay_fen": amount * discount // 100, # 分(实付) + "label": str(it.get("label") or f"{amount // 100} 元"), + }) + return items + + +async def packages(db: Database) -> dict: + """充值套餐(system_configs.recharge_packages 优先,env 兜底,60s 缓存)+ 支付开关。""" + global _pkg_cache + now = time.monotonic() + items: list[dict] | None = None + if _pkg_cache and now - _pkg_cache[0] < _PKG_TTL: + items = _pkg_cache[1] + else: + try: + raw = await db.config.get("recharge_packages") + if raw: + items = _normalize_packages(json.loads(raw)) + except (ValueError, TypeError): # noqa: PERF203 + items = None + if not items: + try: + items = _normalize_packages(json.loads(pay_config.DEFAULT_PACKAGES_JSON)) + except (ValueError, TypeError): + items = [] + _pkg_cache = (now, items) + + enabled = pay_config.pay_enabled() + return { + "enabled": enabled, + "items": [ + { + "id": p["id"], "amount": p["amount"] // 100, "bonus": p["bonus"] // 100, + "discount": p["discount"], "pay": p["pay_fen"] // 100, + "label": p["label"], + "quota_micro": (p["pay_fen"] + p["bonus"]) // 100 * pay_config.QUOTA_PER_YUAN, + } + for p in items + ] if enabled else [], + "min_yuan": pay_config.RECHARGE_MIN_YUAN, + "max_yuan": pay_config.RECHARGE_MAX_YUAN, + } + + +# --------------------------------------------------------------------------- +# 下单 +# --------------------------------------------------------------------------- +async def create_order(db: Database, user: dict, *, client_type: str, + package_id: str = "", amount_yuan: float | None = None) -> dict: + """创建充值订单。 + + client_type: + - "mp"(小程序支付,**统一入口**):不在下单时调微信——桌面端展示小程序码, + 微信扫码自动打开小程序「确认支付」页,由小程序侧按 openid 发起 JSAPI 支付 + (与「小程序扫码登录」同构:桌面出码 → 小程序内确认/支付 → 桌面轮询状态)。 + - "jsapi"(小程序内直充):下单即返回 wx.requestPayment 参数。 + - "native"(保留:桌面直接出微信收款码,需公众号 appid 绑定商户号)。 + """ + if client_type not in ("mp", "native", "jsapi"): + raise ValueError("client_type 仅支持 mp/jsapi/native") + if not pay_config.pay_enabled(): + raise RuntimeError("支付未配置,暂不可用") + + # 1) 金额:套餐固定价,或自由金额(整数分口径) + plist = (await packages(db))["items"] + if package_id: + pkg = next((p for p in plist if p["id"] == package_id), None) + if pkg is None: + raise ValueError("套餐不存在或已下架") + # 实付按折扣计算(discount 100=原价);packages() 返回的 amount/bonus 单位为元 + amount_fen = pkg["amount"] * pkg["discount"] # 元 × % → 分 + bonus_quota_micro = pkg["bonus"] * pay_config.QUOTA_PER_YUAN + else: + amount_fen = int(round(float(amount_yuan or 0) * 100)) + bonus_quota_micro = 0 + if amount_fen <= 0: + raise ValueError("金额必须大于 0") + min_fen = int(round(pay_config.RECHARGE_MIN_YUAN * 100)) + max_fen = int(round(pay_config.RECHARGE_MAX_YUAN * 100)) + if not (min_fen <= amount_fen <= max_fen): + raise ValueError(f"金额需在 {pay_config.RECHARGE_MIN_YUAN:g} ~ {pay_config.RECHARGE_MAX_YUAN:g} 元之间") + + # 2) 复用未过期同金额 pending 单(防重复扫码) + now_iso = utcnow_iso() + dup = await db.compute_recharges.find_pending_same_amount(user["id"], amount_fen, now_iso) + if dup and dup.get("code_url"): + return _order_view(dup) + + # 3) 引擎用户(幂等建号)并解析 engine_user_id + username = user.get("username", "") + await compute_client.ensure_user(username) + engine_user_id = await _resolve_engine_user_id(username) + + # 4) 下单:"mp" 只落库(支付由小程序扫码后发起);native/jsapi 此处即调微信 + order_no = f"{ORDER_PREFIX}{int(time.time())}_{secrets.token_hex(4).upper()}" + expires_at = _iso_in(pay_config.RECHARGE_EXPIRE_MINUTES * 60) + + wx_result: dict = {} + if client_type != "mp": + try: + wx_result = await wxpay.create_order( + client_type=client_type, out_trade_no=order_no, total_fen=amount_fen, + description=f"算力充值 - {amount_fen // 100} 元", openid=user.get("wx_mini_openid", ""), + ) + except RuntimeError as exc: + logger.error("充值下单失败 user=%s: %s", username, exc) + raise ValueError(f"微信下单失败:{exc}") from exc + + order = await db.compute_recharges.create({ + "order_no": order_no, "user_id": user["id"], "username": username, + "engine_user_id": engine_user_id, "package_id": package_id, + "amount_fen": amount_fen, + "quota_micro": amount_fen // 100 * pay_config.QUOTA_PER_YUAN, + "bonus_quota_micro": bonus_quota_micro, + "client_type": client_type, "appid": wx_result.get("appid", ""), + "prepay_id": wx_result.get("prepay_id", ""), "code_url": wx_result.get("code_url", ""), + "expires_at": expires_at, + }) + view = _order_view(order) + if client_type == "jsapi": + view["pay_params"] = wx_result.get("pay_params") # wx.requestPayment 直接参数 + if client_type == "mp": + # 小程序码:微信扫码自动打开小程序「确认支付」页并携带 scene=order_no + # (order_no 22 字符,满足 wxacode scene ≤32 限制) + from ..services import wechat + try: + png = await wechat.get_wxacode(order_no, page="pages/pay/index") + import base64 as _b64 + view["qr_image"] = f"data:image/png;base64,{_b64.b64encode(png).decode('ascii')}" + except wechat.WechatError as exc: + logger.error("生成小程序码失败 order=%s: %s", order_no, exc) + view["qr_image"] = "" + return view + + +async def build_pay_params(db: Database, user: dict, order_no: str) -> dict: + """小程序扫码进入「确认支付」页:校验归属后按当前小程序用户 openid 发起 JSAPI 下单, + 返回订单摘要 + wx.requestPayment 参数。""" + order = await db.compute_recharges.get_by_order_no(order_no) + if order is None: + raise ValueError("订单不存在") + if order["user_id"] != user["id"]: + raise ValueError("请使用下单账号登录的小程序扫码(订单归属不一致)") + if order["status"] != "pending": + return {"order": _order_view(order), "pay_params": None, "finished": True} + if order["expires_at"] and order["expires_at"] < utcnow_iso(): + await db.compute_recharges.mark_status(order_no, "closed") + return {"order": await db.compute_recharges.get_by_order_no(order_no), "pay_params": None, "finished": True} + openid = user.get("wx_mini_openid", "") + if not openid: + raise ValueError("当前账号未绑定小程序微信身份,请先用微信登录小程序") + try: + result = await wxpay.create_order( + client_type="jsapi", out_trade_no=order_no, total_fen=int(order["amount_fen"]), + description=f"算力充值 - {int(order['amount_fen']) // 100} 元", openid=openid, + ) + except RuntimeError as exc: + logger.error("小程序拉起支付失败 order=%s: %s", order_no, exc) + raise ValueError(f"微信下单失败:{exc}") from exc + # 记录 prepay/appid(不改变 pending 状态) + await db.compute_recharges.set_prepay(order_no, result.get("appid", ""), result.get("prepay_id", "")) + return { + "order": _order_view(order), + "pay_params": result.get("pay_params"), + "finished": False, + } + + +def _order_view(order: dict) -> dict: + """对外的订单视图(不含内部载荷)。""" + return { + "order_no": order["order_no"], "amount_fen": order["amount_fen"], + "quota_micro": order["quota_micro"], "bonus_quota_micro": order["bonus_quota_micro"], + "status": order["status"], "code_url": order["code_url"], + "prepay_id": order.get("prepay_id", ""), "expires_at": order["expires_at"], + "created_at": order["created_at"], "paid_at": order.get("paid_at", ""), + } + + +async def _resolve_engine_user_id(username: str) -> int: + """按用户名在引擎侧解析用户 id(失败返回 0,到账时再试)。""" + try: + for page in (1, 2): + for it in await compute_client.list_engine_users(page=page, page_size=100): + if str(it.get("username") or "") == username: + return int(it.get("id") or 0) + except Exception as exc: # noqa: BLE001 + logger.warning("解析引擎用户 id 失败 %s: %s", username, exc) + return 0 + + +# --------------------------------------------------------------------------- +# 回调 / 到账 +# --------------------------------------------------------------------------- +async def handle_notify(db: Database, result: dict) -> bool: + """微信回调分发:仅处理 CR_ 前缀订单;返回 False 让微信重试。""" + out_trade_no = result.get("out_trade_no", "") + if not out_trade_no.startswith(ORDER_PREFIX): + logger.warning("未知订单类型回调: %s", out_trade_no) + return True + return await process_paid_order(db, out_trade_no, result) + + +async def process_paid_order(db: Database, order_no: str, wechat_data: dict) -> bool: + """支付成功 → 引擎加款(幂等)。返回 False = 处理失败(触发微信重试)。""" + order = await db.compute_recharges.get_by_order_no(order_no) + if order is None: + logger.warning("充值订单不存在: %s", order_no) + return True + if order["status"] in ("paid", "credited"): + return True # 已处理(回调重放/并发),幂等成功 + + # 幂等排他:仅 pending → paid 成功者继续到账 + payload_json = json.dumps(wechat_data, ensure_ascii=False)[:8000] + got = await db.compute_recharges.mark_paid(order_no, wechat_data.get("transaction_id", ""), payload_json) + if not got: + return True + + total_micro = int(order["quota_micro"]) + int(order["bonus_quota_micro"]) + try: + engine_user_id = int(order["engine_user_id"] or 0) + if not engine_user_id: + engine_user_id = await _resolve_engine_user_id(order["username"]) + if not engine_user_id: + raise RuntimeError(f"引擎用户未解析: {order['username']}") + await compute_client.adjust_user_quota(engine_user_id, total_micro, "add") + except Exception as exc: # noqa: BLE001 + logger.error("充值到账失败 order=%s: %s", order_no, exc, exc_info=True) + await db.compute_recharges.mark_status(order_no, "failed") + await db.audit.add(action="compute.recharge_failed", resource="compute_recharge_order", + resource_id=order_no, detail=f"{total_micro} micro: {exc}", + user_id=order["user_id"]) + return False + + await db.compute_recharges.mark_credited(order_no) + await compute_client.sync_user_mirror(db, engine_user_id) # best-effort 镜像回写 + await db.audit.add(action="compute.recharge_credited", resource="compute_recharge_order", + resource_id=order_no, detail=f"{total_micro} micro, engine_user={engine_user_id}", + user_id=order["user_id"]) + logger.info("充值到账成功 order=%s user=%s micro=%s", order_no, order["username"], total_micro) + return True + + +# --------------------------------------------------------------------------- +# 对账 / 过期 +# --------------------------------------------------------------------------- +async def reconcile(db: Database, order: dict) -> dict: + """pending 单对账:查微信侧状态,已支付则补单;超时未付置 closed。""" + if order["status"] != "pending": + return order + if order["expires_at"] and order["expires_at"] < utcnow_iso(): + await db.compute_recharges.mark_status(order["order_no"], "closed") + return await db.compute_recharges.get_by_order_no(order["order_no"]) or order + data = await wxpay.query_order(order["order_no"]) + state = (data or {}).get("trade_state", "") + if state == "SUCCESS": + if not await process_paid_order(db, order["order_no"], data): + logger.error("对账补单失败 order=%s", order["order_no"]) + elif state in ("CLOSED", "PAY_ERROR", "REVOKED"): + await db.compute_recharges.mark_status(order["order_no"], "closed") + return await db.compute_recharges.get_by_order_no(order["order_no"]) or order diff --git a/app/pay/wxpay.py b/app/pay/wxpay.py new file mode 100644 index 0000000..f44db69 --- /dev/null +++ b/app/pay/wxpay.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- +"""微信支付 V3 客户端封装(wechatpayv3 AsyncWeChatPay)。 + +- 懒加载单例:首次使用时初始化(不依赖 lifespan),缺配置时返回 False 并记录原因。 +- 下单:Native(桌面端扫码,返回 code_url)/ JSAPI(小程序,返回 wx.requestPayment 参数)。 + wechatpayv3 ``pay()`` 支持按单覆盖 appid → 单实例同时服务双 appid。 +- 回调:异步验签(平台证书缺失/过期自动拉取)+ AEAD_AES_256_GCM 解密。 + +参考实现:PineSoundServer ``routers/userapi/notify/weixin.py``。 +""" +from __future__ import annotations + +import json +import logging +import time +import uuid +from typing import Optional + +from wechatpayv3.async_ import AsyncWeChatPay, WeChatPayType +from wechatpayv3.async_.utils import aes_decrypt + +from . import config as pay_config + +logger = logging.getLogger("pay.wxpay") + +_wxpay: Optional[AsyncWeChatPay] = None +_init_lock = False +_init_error: str = "" + + +async def _ensure_wxpay() -> bool: + """确保 wxpay 已初始化(懒加载 + 简单双检锁)。""" + global _wxpay, _init_lock, _init_error + if _wxpay is not None: + return True + if not pay_config.pay_enabled(): + _init_error = "微信支付商户配置不完整(PINEAGENTS_WECHATPAY_*)" + return False + if _init_lock: + import asyncio + for _ in range(50): # 最多等 5 秒 + if _wxpay is not None: + return True + await asyncio.sleep(0.1) + return False + _init_lock = True + try: + with open(pay_config.WECHATPAY_PRIVATE_KEY_PATH, mode="r") as f: + private_key = f.read() + import os + public_key_path = pay_config.WECHATPAY_PUBLIC_KEY_PATH + _wxpay = AsyncWeChatPay( + wechatpay_type=WeChatPayType.NATIVE, + mchid=pay_config.WECHATPAY_MCHID, + private_key=private_key, + cert_serial_no=pay_config.WECHATPAY_CERT_SERIAL_NO, + appid=pay_config.WECHATPAY_NATIVE_APPID, + apiv3_key=pay_config.WECHATPAY_APIV3_KEY, + notify_url=pay_config.WECHATPAY_NOTIFY_URL, + cert_dir=pay_config.WECHATPAY_CERT_DIR, + logger=logger, + public_key=( + open(public_key_path).read() + if public_key_path and os.path.exists(public_key_path) + else None + ), + public_key_id=pay_config.WECHATPAY_PUBLIC_KEY_ID or None, + ) + await _wxpay.__aenter__() + logger.info("微信支付客户端懒加载初始化成功") + return True + except Exception as exc: # noqa: BLE001 + _init_error = str(exc) + logger.error("微信支付客户端初始化失败: %s", exc, exc_info=True) + return False + finally: + _init_lock = False + + +def init_error() -> str: + """最近一次初始化失败原因(供接口 503 提示)。""" + return _init_error + + +# --------------------------------------------------------------------------- +# 回调验签 + 解密 +# --------------------------------------------------------------------------- +async def verify_and_decrypt(headers, body) -> Optional[dict]: + """验证微信回调签名并解密 resource;失败返回 None。""" + if not await _ensure_wxpay(): + logger.error("wxpay 未初始化,无法验证回调签名: %s", _init_error) + return None + body_str = body.decode("UTF-8") if isinstance(body, bytes) else body + if not await _wxpay._core._verify_signature_async(headers, body_str): + logger.error("回调签名验证失败") + return None + data = json.loads(body_str) + resource = data.get("resource") + if not resource: + logger.error("回调缺少 resource 字段") + return None + try: + decrypted = aes_decrypt( + nonce=resource.get("nonce"), + ciphertext=resource.get("ciphertext"), + associated_data=resource.get("associated_data") or "", + apiv3_key=_wxpay._core._apiv3_key, + ) + except Exception as exc: # noqa: BLE001 + logger.error("回调解密异常: %s", exc) + return None + if not decrypted: + logger.error("回调解密失败(检查 APIv3 密钥)") + return None + data.update({"resource": json.loads(decrypted)}) + return data + + +# --------------------------------------------------------------------------- +# 下单 / 查单 +# --------------------------------------------------------------------------- +async def create_order(*, client_type: str, out_trade_no: str, total_fen: int, + description: str, openid: str = "") -> dict: + """统一下单。 + + - native → {"code_url": ...}(桌面端扫码) + - jsapi → {"pay_params": {appId,timeStamp,nonceStr,package,signType,paySign}}(小程序) + 失败抛 RuntimeError(携带微信返回报文摘要)。 + """ + if not await _ensure_wxpay(): + raise RuntimeError(f"微信支付客户端未就绪: {_init_error or '未配置'}") + if client_type == "jsapi": + if not openid: + raise RuntimeError("JSAPI 支付缺少 openid") + appid = pay_config.WECHATPAY_JSAPI_APPID + pay_type = WeChatPayType.MINIPROG + payer = {"openid": openid} + else: + appid = pay_config.WECHATPAY_NATIVE_APPID + pay_type = WeChatPayType.NATIVE + payer = None + + code, result = await _wxpay.pay( + description=description, + out_trade_no=out_trade_no, + amount={"total": int(total_fen), "currency": "CNY"}, + payer=payer, + pay_type=pay_type, + appid=appid, + ) + if code != 200: + raise RuntimeError(f"微信下单失败 http={code}: {str(result)[:300]}") + data = result if isinstance(result, dict) else json.loads(result) + + if client_type == "jsapi": + prepay_id = data.get("prepay_id") or "" + if not prepay_id: + raise RuntimeError(f"微信下单无 prepay_id: {str(data)[:300]}") + return {"appid": appid, "prepay_id": prepay_id, "pay_params": _jsapi_params(appid, prepay_id)} + code_url = data.get("code_url") or "" + if not code_url: + raise RuntimeError(f"微信下单无 code_url: {str(data)[:300]}") + return {"appid": appid, "code_url": code_url} + + +def _jsapi_params(appid: str, prepay_id: str) -> dict: + """组装小程序 wx.requestPayment 参数(RSA 签名:appid\ntimeStamp\nnonceStr\npackage)。""" + timeStamp = str(int(time.time())) + nonceStr = uuid.uuid4().hex + package = f"prepay_id={prepay_id}" + # wechatpayv3 sign() 接收列表并自行 '\n'.join(data) + '\n',勿传已拼接字符串 + sign = _wxpay.sign([appid, timeStamp, nonceStr, package]) + return { + "timeStamp": timeStamp, + "nonceStr": nonceStr, + "package": package, + "signType": "RSA", + "paySign": sign, + } + + +async def query_order(out_trade_no: str) -> Optional[dict]: + """主动查单(对账补单用);微信侧无此单返回 None。""" + if not await _ensure_wxpay(): + return None + try: + code, result = await _wxpay.query(out_trade_no=out_trade_no) + except Exception as exc: # noqa: BLE001 + logger.warning("查单异常 order=%s: %s", out_trade_no, exc) + return None + if code != 200: + return None + return result if isinstance(result, dict) else json.loads(result) diff --git a/pyproject.toml b/pyproject.toml index 936f492..ebf05c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ "torch>=2.13.0", "transformers>=5.15.1", "alembic>=1.19.1", + "aiofiles>=24.1.0", ] [dependency-groups] diff --git a/uv.lock b/uv.lock index 67217e7..79bba85 100644 --- a/uv.lock +++ b/uv.lock @@ -1962,6 +1962,7 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "aioboto3" }, + { name = "aiofiles" }, { name = "aiosqlite" }, { name = "alembic" }, { name = "asyncmy" }, @@ -1998,6 +1999,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "aioboto3", specifier = ">=13.0.0" }, + { name = "aiofiles", specifier = ">=24.1.0" }, { name = "aiosqlite", specifier = ">=0.20.0" }, { name = "alembic", specifier = ">=1.19.1" }, { name = "asyncmy", specifier = ">=0.2.9" }, diff --git a/wechatpayv3/__init__.py b/wechatpayv3/__init__.py new file mode 100644 index 0000000..46ebbd3 --- /dev/null +++ b/wechatpayv3/__init__.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- + +from .type import SignType, WeChatPayType + + +class WeChatPay(): + def __init__(self, + wechatpay_type, + mchid, + private_key, + cert_serial_no, + appid, + apiv3_key, + notify_url=None, + cert_dir=None, + logger=None, + partner_mode=False, + proxy=None, + timeout=None, + public_key=None, + public_key_id=None): + """ + :param wechatpay_type: 微信支付类型,示例值:WeChatPayType.MINIPROG + :param mchid: 直连商户号,示例值:'1230000109' + :param private_key: 商户证书私钥,示例值:'MIIEvwIBADANBgkqhkiG9w0BAQE...' + :param cert_serial_no: 商户证书序列号,示例值:'444F4864EA9B34415...' + :param appid: 应用ID,示例值:'wxd678efh567hg6787' + :param apiv3_key: 商户APIv3密钥,示例值:'a12d3924fd499edac8a5efc...' + :param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php' + :param cert_dir: 平台证书存放目录,示例值:'/server/cert' + :param logger: 日志记录器,示例值logging.getLoger('demo') + :param partner_mode: 接入模式,默认False为直连商户模式,True为服务商模式 + :param proxy: 代理设置,示例值:{"https": "http://10.10.1.10:1080"} + :param timeout: 超时时间,示例值:(10, 30), 10为建立连接的最大超时时间,30为读取响应的最大超时实践 + :param public_key: 微信支付平台公钥,示例值:'MIIEvwIBADANBgkqhkiG9w0BAQE...' + :param public_key_id: 微信支付平台公钥id,示例值:'PUB_KEY_ID_444F4864EA9B34415...' + """ + from .core import Core + + self._type = wechatpay_type + self._mchid = mchid + self._appid = appid + self._notify_url = notify_url + self._core = Core(mchid=self._mchid, + cert_serial_no=cert_serial_no, + private_key=private_key, + apiv3_key=apiv3_key, + cert_dir=cert_dir, + logger=logger, + proxy=proxy, + timeout=timeout, + public_key=public_key, + public_key_id=public_key_id) + self._partner_mode = partner_mode + + def sign(self, data, sign_type=SignType.RSA_SHA256): + """使用RSAwithSHA256或HMAC_256算法计算签名值供调起支付时使用 + :param data: 需要签名的参数清单 + :微信支付订单采用RSAwithSHA256算法时,示例值:['wx888','1414561699','5K8264ILTKCH16CQ2502S....','prepay_id=wx201410272009395522657....'] + :微信支付分订单采用HMAC_SHA256算法时,示例值:{'mch_id':'1230000109','service_id':'88888888000011','out_order_no':'1234323JKHDFE1243252'} + """ + return self._core.sign(data, sign_type) + + def decrypt_callback(self, headers, body): + """解密回调接口收到的信息,仅返回resource解密后的参数字符串,此接口为兼容旧版本而保留,建议调用callback() + :param headers: 回调接口收到的headers + :param body: 回调接口收到的body + """ + return self._core.decrypt_callback(headers, body) + + def callback(self, headers, body): + """解密回调接口收到的信息,返回所有传入的参数 + :param headers: 回调接口收到的headers + :param body: 回调接口收到的body + """ + return self._core.callback(headers, body) + + def decrypt(self, ciphtext): + """解密微信支付平台返回的信息中的敏感字段 + :param ciphtext: 加密后的敏感字段,示例值:'Qe41VhP/sGdNeTHMQGlxCWiUyHu6XNO9GCYln2Luv4HhwJzZBfcL12sB+PgZcS5NhePBog30NgJ1xRaK+gbGDKwpg==' + """ + return self._core.decrypt(ciphtext) + + from .apply4subject import (apply4subject_cancel, apply4subject_query, + apply4subject_state, apply4subject_submit) + from .applyment import (applyment_query, applyment_settlement_modify, + applyment_settlement_query, applyment_submit) + from .businesscircle import (business_parking_sync, business_point_status, + points_notify, user_authorization) + from .capital import (capital_branches, capital_cities, + capital_corporate_banks, capital_personal_banks, + capital_provinces, capital_search_bank_number) + from .complaint import (complaint_complete, complaint_detail_query, + complaint_history_query, complaint_image_download, + complaint_image_upload, complaint_list_query, + complaint_notification_create, + complaint_notification_delete, + complaint_notification_query, + complaint_notification_update, complaint_response, + complaint_update_refund) + from .fapiao import (fapiao_applications, fapiao_card_template, + fapiao_check_submch, fapiao_download_file, + fapiao_insert_cards, fapiao_merchant_base_info, + fapiao_merchant_config, fapiao_query, + fapiao_query_files, fapiao_reverse, + fapiao_set_merchant_config, fapiao_tax_codes, + fapiao_title, fapiao_title_url, fapiao_upload_file) + from .goldplan import (goldplan_advertising_close, + goldplan_advertising_filter, + goldplan_advertising_open, + goldplan_custompage_change, goldplan_plan_change) + from .marketing import (marketing_busifavor_callback_query, + marketing_busifavor_callback_update, + marketing_busifavor_coupon_associate, + marketing_busifavor_coupon_deactivate, + marketing_busifavor_coupon_detail, + marketing_busifavor_coupon_disassociate, + marketing_busifavor_coupon_return, + marketing_busifavor_coupon_use, + marketing_busifavor_couponcode_upload, + marketing_busifavor_stock_budget, + marketing_busifavor_stock_create, + marketing_busifavor_stock_modify, + marketing_busifavor_stock_query, + marketing_busifavor_subsidy_pay, + marketing_busifavor_subsidy_query, + marketing_busifavor_user_coupon, + marketing_card_send, + marketing_favor_callback_update, + marketing_favor_coupon_detail, + marketing_favor_refund_flow, + marketing_favor_stock_create, + marketing_favor_stock_detail, + marketing_favor_stock_item, + marketing_favor_stock_list, + marketing_favor_stock_merchant, + marketing_favor_stock_pause, + marketing_favor_stock_restart, + marketing_favor_stock_send, + marketing_favor_stock_start, + marketing_favor_use_flow, + marketing_favor_user_coupon, + marketing_image_upload, + marketing_partnership_build, + marketing_partnership_query, + marketing_paygift_activity_create, + marketing_paygift_activity_detail, + marketing_paygift_activity_list, + marketing_paygift_activity_terminate, + marketing_paygift_goods_list, + marketing_paygift_merchant_add, + marketing_paygift_merchant_delete, + marketing_paygift_merchants_list) + from .media import image_upload, video_upload + from .merchantrisk import (merchantrisk_callback_create, + merchantrisk_callback_delete, + merchantrisk_callback_query, + merchantrisk_callback_update) + from .parking import (parking_enter, parking_order, parking_order_query, + parking_service_find) + from .payscore import (payscore_cancel, payscore_complete, payscore_create, + payscore_direct_complete, payscore_merchant_bill, + payscore_modify, payscore_pay, payscore_permission, + payscore_permission_query, + payscore_permission_terminate, payscore_query, + payscore_refund, payscore_refund_query, + payscore_sync) + from .profitsharing import (brand_profitsharing_add_receiver, + brand_profitsharing_amount_query, + brand_profitsharing_config_query, + brand_profitsharing_delete_receiver, + brand_profitsharing_order, + brand_profitsharing_order_query, + brand_profitsharing_return, + brand_profitsharing_return_query, + brand_profitsharing_unfreeze, + profitsharing_add_receiver, + profitsharing_amount_query, profitsharing_bill, + profitsharing_config_query, + profitsharing_delete_receiver, + profitsharing_order, profitsharing_order_query, + profitsharing_return, + profitsharing_return_query, + profitsharing_unfreeze) + from .smartguide import (guides_assign, guides_query, guides_register, + guides_update) + from .transaction import (abnormal_refund, close, codepay_reverse, combine_close, + combine_pay, combine_query, download_bill, + fundflow_bill, pay, query, query_refund, refund, + submch_fundflow_bill, trade_bill) + from .transfer import (transfer_batch, transfer_bill_receipt, + transfer_detail_receipt, transfer_query_batchid, + transfer_query_bill_receipt, + transfer_query_detail_id, + transfer_query_out_batch_no, + transfer_query_out_detail_no, + transfer_query_receipt) + from .mchtransfer import (mch_transfer_bills, mch_transfer_bills_cancel, + mch_transfer_bills_query, mch_transfer_elecsign, + mch_transfer_elecsign_query) diff --git a/wechatpayv3/apply4subject.py b/wechatpayv3/apply4subject.py new file mode 100644 index 0000000..9f67b7a --- /dev/null +++ b/wechatpayv3/apply4subject.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType + + +def apply4subject_submit(self, business_code, contact_info, subject_info, identification_info, channel_id=None, addition_info=None, ubo_info_list=[]): + """(商户开户意愿)提交申请单 + :param business_code: 业务申请编号,示例值:'APPLYMENT_00000000001' + :param contact_info: 联系人信息,示例值:{'name':'张三','id_card_number':'320311770706001','mobile':'13900000000'} + :param subject_info: 主体信息,示例值:{'subject_type':'SUBJECT_TYPE_ENTERPRISE','business_license_info':{'license_copy':'demo-media-id','license_number':'123456789012345678','merchant_name':'腾讯科技有限公司','legal_person':'张三','company_address':'广东省深圳市南山区xx路xx号','licence_valid_date':'["1970-01-01","forever"]'}} + :param identification_info: 法人身份信息,示例值:{'identification_type':'IDENTIFICATION_TYPE_IDCARD','identification_name':'张三','identification_number':'110220330044005500','identification_valid_date':'["1970-01-01","forever"]','identification_front_copy':'0P3ng6KTIW4-Q_l2FjKLZ...','identification_back_copy':'0P3ng6KTIW4-Q_l2FjKLZ...'} + :param channel_id: 渠道商户号,示例值:'20001111' + :param addition_info: 补充材料,示例值:{'confirm_mchid_list':['20001113']} + :param ubo_info_list: 最终受益人信息列表,示例值:[{'ubo_id_doc_type':'IDENTIFICATION_TYPE_IDCARD','ubo_id_doc_name':'张三','ubo_id_doc_number':'110220330044005500'}] + """ + params = {} + if business_code: + params.update({'business_code': business_code}) + else: + raise Exception('business_code is not assigned.') + if contact_info: + params.update({'contact_info': contact_info}) + else: + raise Exception('contact_info is not assigned.') + if subject_info: + params.update({'subject_info': subject_info}) + else: + raise Exception('subject_info is not assigned.') + if identification_info: + params.update({'identification_info': identification_info}) + else: + raise Exception('identification_info is not assigned') + if channel_id: + params.update({'channel_id': channel_id}) + if addition_info: + params.update({'addition_info': addition_info}) + if ubo_info_list: + params.update({'ubo_info_list': ubo_info_list}) + contact_name = params.get('contact_info').get('name') + if contact_name: + params['contact_info']['name'] = self._core.encrypt(contact_name) + contact_mobile = params.get('contact_info').get('mobile') + if contact_mobile: + params['contact_info']['mobile'] = self._core.encrypt(contact_mobile) + contact_number = params.get('contact_info').get('id_card_number') + if contact_number: + params['contact_info']['id_card_number'] = self._core.encrypt(contact_number) + identification_name = params.get('identification_info').get('identification_name') + if identification_name: + params['identification_info']['identification_name'] = self._core.encrypt(identification_name) + identification_number = params.get('identification_info').get('identification_number') + if identification_number: + params['identification_info']['identification_number'] = self._core.encrypt(identification_number) + identification_address = params.get('identification_info').get('identification_address') + if identification_address: + params['identification_info']['identification_address'] = self._core.encrypt(identification_address) + if params.get('ubo_info_list'): + for ubo_info in params['ubo_info_list']: + ubo_info['ubo_id_doc_name'] = self._core.encrypt(ubo_info['ubo_id_doc_name']) + ubo_info['ubo_id_doc_number'] = self._core.encrypt(ubo_info['ubo_id_doc_number']) + ubo_info['ubo_id_doc_address'] = self._core.encrypt(ubo_info['ubo_id_doc_address']) + path = '/v3/apply4subject/applyment' + return self._core.request(path, method=RequestType.POST, data=params, cipher_data=True) + + +def apply4subject_cancel(self, business_code=None, applyment_id=None): + """(商户开户意愿)撤销申请单 + :param business_code: 业务申请编号,示例值:'2000001234567890' + :param applyment_id: 申请单编号,示例值:2000001234567890 + """ + if business_code: + path = '/v3/apply4subject/applyment/%s/cancel' % business_code + elif applyment_id: + path = '/v3/apply4subject/applyment/%s/cancel' % applyment_id + else: + raise Exception('business_code or applyment_id is not assigned.') + return self._core.request(path) + + +def apply4subject_query(self, business_code=None, applyment_id=None): + """(商户开户意愿)查询申请单审核结果 + :param business_code: 业务申请编号,示例值:'2000001234567890' + :param applyment_id: 申请单编号,示例值:2000001234567890 + """ + if business_code: + path = '/v3/apply4subject/applyment?business_code=%s' % business_code + elif applyment_id: + path = '/v3/apply4subject/applyment?applyment_id=%s' % applyment_id + else: + raise Exception('business_code or applyment_id is not assigned.') + return self._core.request(path) + + +def apply4subject_state(self, sub_mchid): + """(商户开户意愿)获取商户开户意愿确认状态 + :param sub_mchid: 特约商户号,示例值:'1511101111' + """ + if sub_mchid: + path = '/v3/apply4subject/applyment/merchants/%s/state' % sub_mchid + else: + raise Exception('sub_mchid is not assigned.') + return self._core.request(path) diff --git a/wechatpayv3/applyment.py b/wechatpayv3/applyment.py new file mode 100644 index 0000000..23e68a0 --- /dev/null +++ b/wechatpayv3/applyment.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType + + +def applyment_submit(self, business_code, contact_info, subject_info, business_info, settlement_info, bank_account_info, addition_info=None): + """提交申请单 + https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter10_1_1.shtml + :param business_code: 业务申请编号,示例值:'APPLYMENT_00000000001' + :param contact_info: 超级管理员信息,示例值:{'contact_name':'张三','contact_id_number':'320311770706001','mobile_phone':'13900000000','contact_email':'admin@demo.com'} + :param subject_info: 主体资料,示例值:{'subject_type':'SUBJECT_TYPE_ENTERPRISE','business_license_info':{'license_copy':'demo-media-id','license_number':'123456789012345678','merchant_name':'腾讯科技有限公司','legal_person':'张三'},'identity_info':{'id_doc_type':'IDENTIFICATION_TYPE_IDCARD','id_card_info'{'id_card_copy':'demo-media-id'}}} + :param business_info: 经营资料,示例值:{'merchant_shortname':'张三餐饮店','service_phone':'0758xxxxxx','sales_info':{'sales_scenes_type':['SALES_SCENES_STORE','SALES_SCENES_MP']}} + :param settlement_info: 结算规则,示例值:{'settlement_id':'719','qualification_type':'餐饮'} + :param bank_account_info: 结算银行账户,示例值:{'bank_account_type':'BANK_ACCOUNT_TYPE_CORPORATE','account_name':'xx公司','account_bank':'工商银行','bank_address_code':'110000','account_number':'1234567890'} + :param addition_info: 补充材料,示例值:{'legal_person_commitment':'demo-media-id'} + """ + params = {} + if business_code: + params.update({'business_code': business_code}) + else: + raise Exception('business_code is not assigned.') + if contact_info: + params.update({'contact_info': contact_info}) + else: + raise Exception('contact_info is not assigned.') + if subject_info: + params.update({'subject_info': subject_info}) + else: + raise Exception('subject_info is not assigned.') + if business_info: + params.update({'business_info': business_info}) + else: + raise Exception('business_info is not assigned') + if settlement_info: + params.update({'settlement_info': settlement_info}) + else: + raise Exception('settlement_info is not assigned.') + if bank_account_info: + params.update({'bank_account_info': bank_account_info}) + else: + raise Exception('bank_account_info is not assigned.') + if addition_info: + params.update({'addition_info': addition_info}) + if params.get('contact_info').get('contact_name'): + params['contact_info']['contact_name'] = self._core.encrypt(params['contact_info']['contact_name']) + if params.get('contact_info').get('contact_id_number'): + params['contact_info']['contact_id_number'] = self._core.encrypt(params['contact_info']['contact_id_number']) + if params.get('contact_info').get('openid'): + params['contact_info']['openid'] = self._core.encrypt(params['contact_info']['openid']) + if params.get('contact_info').get('mobile_phone'): + params['contact_info']['mobile_phone'] = self._core.encrypt(params['contact_info']['mobile_phone']) + if params.get('contact_info').get('contact_email'): + params['contact_info']['contact_email'] = self._core.encrypt(params['contact_info']['contact_email']) + id_card_name = params.get('subject_info').get('identity_info').get('id_card_info', {}).get('id_card_name') + if id_card_name: + params['subject_info']['identity_info']['id_card_info']['id_card_name'] = self._core.encrypt(id_card_name) + id_card_number = params.get('subject_info').get('identity_info').get('id_card_info', {}).get('id_card_number') + if id_card_number: + params['subject_info']['identity_info']['id_card_info']['id_card_number'] = self._core.encrypt(id_card_number) + id_card_address = params.get('subject_info').get('identity_info').get('id_card_info', {}).get('id_card_address') + if id_card_address: + params['subject_info']['identity_info']['id_card_info']['id_card_address'] = self._core.encrypt(id_card_address) + id_doc_name = params.get('subject_info').get('identity_info').get('id_doc_info', {}).get('id_doc_name') + if id_doc_name: + params['subject_info']['identity_info']['id_doc_info']['id_doc_name'] = self._core.encrypt(id_doc_name) + id_doc_number = params.get('subject_info').get('identity_info').get('id_doc_info', {}).get('id_doc_number') + if id_doc_number: + params['subject_info']['identity_info']['id_doc_info']['id_doc_number'] = self._core.encrypt(id_doc_number) + id_doc_address = params.get('subject_info').get('identity_info').get('id_doc_info', {}).get('id_doc_address') + if id_doc_address: + params['subject_info']['identity_info']['id_doc_info']['id_doc_address'] = self._core.encrypt(id_doc_address) + if params.get('subject_info').get('ubo_info_list'): + for ubo_info in params['subject_info']['ubo_info_list']: + ubo_info['ubo_id_doc_name'] = self._core.encrypt(ubo_info['ubo_id_doc_name']) + ubo_info['ubo_id_doc_number'] = self._core.encrypt(ubo_info['ubo_id_doc_number']) + ubo_info['ubo_id_doc_address'] = self._core.encrypt(ubo_info['ubo_id_doc_address']) + params['bank_account_info']['account_name'] = self._core.encrypt(params['bank_account_info']['account_name']) + params['bank_account_info']['account_number'] = self._core.encrypt(params['bank_account_info']['account_number']) + path = '/v3/applyment4sub/applyment/' + return self._core.request(path, method=RequestType.POST, data=params, cipher_data=True) + + +def applyment_query(self, business_code=None, applyment_id=None): + """查询申请单状态 + https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter11_1_2.shtml + :param business_code: 业务申请编号,示例值:'APPLYMENT_00000000001' + :param applyment_id: 申请单号,示例值:2000001234567890 + """ + if business_code: + path = '/v3/applyment4sub/applyment/business_code/%s' % business_code + elif applyment_id: + path = '/v3/applyment4sub/applyment/applyment_id/%s' % applyment_id + else: + raise Exception('business_code or applyment_id is not assigned.') + return self._core.request(path) + + +def applyment_settlement_modify(self, sub_mchid, account_type, account_bank, bank_address_code, account_number, bank_name=None, bank_branch_id=None): + """修改结算账号 + https://pay.weixin.qq.com/docs/partner/apis/modify-settlement/sub-merchants/modify-settlement.html + :param sub_mchid: 特约商户号,示例值:'1511101111' + :param account_type: 账户类型,枚举值:'ACCOUNT_TYPE_BUSINESS':对公银行账户,'ACCOUNT_TYPE_PRIVATE':经营者个人银行卡。示例值:'ACCOUNT_TYPE_BUSINESS' + :param account_bank: 开户银行,示例值:'工商银行' + :param bank_address_code: 开户银行省市编码,示例值:'110000' + :param account_number: 银行账号,示例值:'1234567890' + :param bank_name: 开户银行全称(含支行),示例值:'施秉县农村信用合作联社城关信用社' + :param bank_branch_id: 开户银行联行号,示例值:'402713354941' + """ + params = {} + if sub_mchid: + path = '/v3/apply4sub/sub_merchants/%s/modify-settlement' % sub_mchid + else: + raise Exception('sub_mchid is not assigned.') + if account_type: + params.update({'account_type': account_type}) + else: + raise Exception('account_type is not assigned.') + if account_bank: + params.update({'account_bank': account_bank}) + else: + raise Exception('account_bank is not assigned.') + if bank_address_code: + params.update({'bank_address_code': bank_address_code}) + else: + raise Exception('bank_address_code is not assigned.') + cipher_data = False + if account_number: + params.update({'account_number': self._core.encrypt(account_number)}) + cipher_data = True + else: + raise Exception('account_number is not assigned.') + if bank_name: + params.update({'bank_name': bank_name}) + if bank_branch_id: + params.update({'bank_branch_id': bank_branch_id}) + return self._core.request(path, method=RequestType.POST, data=params, cipher_data=cipher_data) + + +def applyment_settlement_query(self, sub_mchid): + """查询结算账户 + https://pay.weixin.qq.com/docs/partner/apis/modify-settlement/sub-merchants/get-settlement.html + :param sub_mchid: 特约商户号,示例值:'1511101111' + """ + if sub_mchid: + path = '/v3/apply4sub/sub_merchants/%s/settlement' % sub_mchid + else: + raise Exception('sub_mchid is not assigned.') + return self._core.request(path) + + +def applyment_settlement_modify_state(self, sub_mchid, application_no): + """查询结算账户修改申请状态 + https://pay.weixin.qq.com/docs/partner/apis/modify-settlement/sub-merchants/get-application.html + :param sub_mchid: 【特约商户/二级商户号】 请填写本服务商负责进件的特约商户/二级商户号。 + :param application_no: 【修改结算账户申请单号】 提交二级商户修改结算账户申请后,由微信支付返回的单号,作为查询申请状态的唯一标识。 + """ + if not (sub_mchid and application_no): + raise Exception('sub_mchid and/or application_no is not assigned.') + path = '/v3/apply4sub/sub_merchants/%s/application/%s' % (sub_mchid, application_no) + return self._core.request(path) diff --git a/wechatpayv3/async_/__init__.py b/wechatpayv3/async_/__init__.py new file mode 100644 index 0000000..60dc0c4 --- /dev/null +++ b/wechatpayv3/async_/__init__.py @@ -0,0 +1,289 @@ +# -*- coding: utf-8 -*- + +from .type import SignType, WeChatPayType + + +class AsyncWeChatPay: + def __init__( + self, + wechatpay_type, + mchid, + private_key, + cert_serial_no, + appid, + apiv3_key, + notify_url=None, + cert_dir=None, + logger=None, + partner_mode=False, + proxy=None, + timeout=None, + public_key=None, + public_key_id=None, + ): + """ + :param wechatpay_type: 微信支付类型,示例值:WeChatPayType.MINIPROG + :param mchid: 直连商户号,示例值:'1230000109' + :param private_key: 商户证书私钥,示例值:'MIIEvwIBADANBgkqhkiG9w0BAQE...' + :param cert_serial_no: 商户证书序列号,示例值:'444F4864EA9B34415...' + :param appid: 应用ID,示例值:'wxd678efh567hg6787' + :param apiv3_key: 商户APIv3密钥,示例值:'a12d3924fd499edac8a5efc...' + :param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php' + :param cert_dir: 平台证书存放目录,示例值:'/server/cert' + :param logger: 日志记录器,示例值logging.getLoger('demo') + :param partner_mode: 接入模式,默认False为直连商户模式,True为服务商模式 + :param proxy: 代理设置,示例值:{"https": "http://10.10.1.10:1080"} + :param timeout: 超时时间,示例值:(10, 30), 10为建立连接的最大超时时间,30为读取响应的最大超时实践 + :param public_key: 微信支付平台公钥,示例值:'MIIEvwIBADANBgkqhkiG9w0BAQE...' + :param public_key_id: 微信支付平台公钥id,示例值:'PUB_KEY_ID_444F4864EA9B34415...' + """ + from .core import AsyncCore + + self._type = wechatpay_type + self._mchid = mchid + self._appid = appid + self._notify_url = notify_url + self._core = AsyncCore( + mchid=self._mchid, + cert_serial_no=cert_serial_no, + private_key=private_key, + apiv3_key=apiv3_key, + cert_dir=cert_dir, + logger=logger, + proxy=proxy, + timeout=timeout, + public_key=public_key, + public_key_id=public_key_id, + ) + self._partner_mode = partner_mode + + async def __aenter__(self): + """Async context manager entry""" + await self._core.__aenter__() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit""" + return await self._core.__aexit__(exc_type, exc_val, exc_tb) + + def sign(self, data, sign_type=SignType.RSA_SHA256): + """使用RSAwithSHA256或HMAC_256算法计算签名值供调起支付时使用 + :param data: 需要签名的参数清单 + :微信支付订单采用RSAwithSHA256算法时,示例值:['wx888','1414561699','5K8264ILTKCH16CQ2502S....','prepay_id=wx201410272009395522657....'] + :微信支付分订单采用HMAC_SHA256算法时,示例值:{'mch_id':'1230000109','service_id':'88888888000011','out_order_no':'1234323JKHDFE1243252'} + """ + return self._core.sign(data, sign_type) + + def decrypt_callback(self, headers, body): + """解密回调接口收到的信息,仅返回resource解密后的参数字符串,此接口为兼容旧版本而保留,建议调用callback() + :param headers: 回调接口收到的headers + :param body: 回调接口收到的body + """ + return self._core.decrypt_callback(headers, body) + + def callback(self, headers, body): + """解密回调接口收到的信息,返回所有传入的参数 + :param headers: 回调接口收到的headers + :param body: 回调接口收到的body + """ + return self._core.callback(headers, body) + + def decrypt(self, ciphtext): + """解密微信支付平台返回的信息中的敏感字段 + :param ciphtext: 加密后的敏感字段,示例值:'Qe41VhP/sGdNeTHMQGlxCWiUyHu6XNO9GCYln2Luv4HhwJzZBfcL12sB+PgZcS5NhePBog30NgJ1xRaK+gbGDKwpg==' + """ + return self._core.decrypt(ciphtext) + + from .apply4subject import ( + apply4subject_cancel, + apply4subject_query, + apply4subject_state, + apply4subject_submit, + ) + from .applyment import ( + applyment_query, + applyment_settlement_modify, + applyment_settlement_query, + applyment_submit, + ) + from .businesscircle import ( + business_parking_sync, + business_point_status, + points_notify, + user_authorization, + ) + from .capital import ( + capital_branches, + capital_cities, + capital_corporate_banks, + capital_personal_banks, + capital_provinces, + capital_search_bank_number, + ) + from .complaint import ( + complaint_complete, + complaint_detail_query, + complaint_history_query, + complaint_image_download, + complaint_image_upload, + complaint_list_query, + complaint_notification_create, + complaint_notification_delete, + complaint_notification_query, + complaint_notification_update, + complaint_response, + complaint_update_refund, + ) + from .fapiao import ( + fapiao_applications, + fapiao_card_template, + fapiao_check_submch, + fapiao_download_file, + fapiao_insert_cards, + fapiao_merchant_base_info, + fapiao_merchant_config, + fapiao_query, + fapiao_query_files, + fapiao_reverse, + fapiao_set_merchant_config, + fapiao_tax_codes, + fapiao_title, + fapiao_title_url, + fapiao_upload_file, + ) + from .goldplan import ( + goldplan_advertising_close, + goldplan_advertising_filter, + goldplan_advertising_open, + goldplan_custompage_change, + goldplan_plan_change, + ) + from .marketing import ( + marketing_busifavor_callback_query, + marketing_busifavor_callback_update, + marketing_busifavor_coupon_associate, + marketing_busifavor_coupon_deactivate, + marketing_busifavor_coupon_detail, + marketing_busifavor_coupon_disassociate, + marketing_busifavor_coupon_return, + marketing_busifavor_coupon_use, + marketing_busifavor_couponcode_upload, + marketing_busifavor_stock_budget, + marketing_busifavor_stock_create, + marketing_busifavor_stock_modify, + marketing_busifavor_stock_query, + marketing_busifavor_subsidy_pay, + marketing_busifavor_subsidy_query, + marketing_busifavor_user_coupon, + marketing_card_send, + marketing_favor_callback_update, + marketing_favor_coupon_detail, + marketing_favor_refund_flow, + marketing_favor_stock_create, + marketing_favor_stock_detail, + marketing_favor_stock_item, + marketing_favor_stock_list, + marketing_favor_stock_merchant, + marketing_favor_stock_pause, + marketing_favor_stock_restart, + marketing_favor_stock_send, + marketing_favor_stock_start, + marketing_favor_use_flow, + marketing_favor_user_coupon, + marketing_image_upload, + marketing_partnership_build, + marketing_partnership_query, + marketing_paygift_activity_create, + marketing_paygift_activity_detail, + marketing_paygift_activity_list, + marketing_paygift_activity_terminate, + marketing_paygift_goods_list, + marketing_paygift_merchant_add, + marketing_paygift_merchant_delete, + marketing_paygift_merchants_list, + ) + from .media import image_upload, video_upload + from .merchantrisk import ( + merchantrisk_callback_create, + merchantrisk_callback_delete, + merchantrisk_callback_query, + merchantrisk_callback_update, + ) + from .parking import ( + parking_enter, + parking_order, + parking_order_query, + parking_service_find, + ) + from .payscore import ( + payscore_cancel, + payscore_complete, + payscore_create, + payscore_direct_complete, + payscore_merchant_bill, + payscore_modify, + payscore_pay, + payscore_permission, + payscore_permission_query, + payscore_permission_terminate, + payscore_query, + payscore_refund, + payscore_refund_query, + payscore_sync, + ) + from .profitsharing import ( + brand_profitsharing_add_receiver, + brand_profitsharing_amount_query, + brand_profitsharing_config_query, + brand_profitsharing_delete_receiver, + brand_profitsharing_order, + brand_profitsharing_order_query, + brand_profitsharing_return, + brand_profitsharing_return_query, + brand_profitsharing_unfreeze, + profitsharing_add_receiver, + profitsharing_amount_query, + profitsharing_bill, + profitsharing_config_query, + profitsharing_delete_receiver, + profitsharing_order, + profitsharing_order_query, + profitsharing_return, + profitsharing_return_query, + profitsharing_unfreeze, + ) + from .smartguide import guides_assign, guides_query, guides_register, guides_update + from .transaction import ( + abnormal_refund, + close, + codepay_reverse, + combine_close, + combine_pay, + combine_query, + download_bill, + fundflow_bill, + pay, + query, + query_refund, + refund, + submch_fundflow_bill, + trade_bill, + ) + from .transfer import ( + transfer_batch, + transfer_bill_receipt, + transfer_detail_receipt, + transfer_query_batchid, + transfer_query_bill_receipt, + transfer_query_detail_id, + transfer_query_out_batch_no, + transfer_query_out_detail_no, + transfer_query_receipt, + ) + from .mchtransfer import ( + mch_transfer_bills, + mch_transfer_bills_cancel, + mch_transfer_bills_query, + mch_transfer_elecsign, + mch_transfer_elecsign_query, + ) diff --git a/wechatpayv3/async_/apply4subject.py b/wechatpayv3/async_/apply4subject.py new file mode 100644 index 0000000..ca2c4d3 --- /dev/null +++ b/wechatpayv3/async_/apply4subject.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType + + +async def apply4subject_submit(self, business_code, contact_info, subject_info, identification_info, channel_id=None, addition_info=None, ubo_info_list=[]): + """(商户开户意愿)提交申请单 + :param business_code: 业务申请编号,示例值:'APPLYMENT_00000000001' + :param contact_info: 联系人信息,示例值:{'name':'张三','id_card_number':'320311770706001','mobile':'13900000000'} + :param subject_info: 主体信息,示例值:{'subject_type':'SUBJECT_TYPE_ENTERPRISE','business_license_info':{'license_copy':'demo-media-id','license_number':'123456789012345678','merchant_name':'腾讯科技有限公司','legal_person':'张三','company_address':'广东省深圳市南山区xx路xx号','licence_valid_date':'["1970-01-01","forever"]'}} + :param identification_info: 法人身份信息,示例值:{'identification_type':'IDENTIFICATION_TYPE_IDCARD','identification_name':'张三','identification_number':'110220330044005500','identification_valid_date':'["1970-01-01","forever"]','identification_front_copy':'0P3ng6KTIW4-Q_l2FjKLZ...','identification_back_copy':'0P3ng6KTIW4-Q_l2FjKLZ...'} + :param channel_id: 渠道商户号,示例值:'20001111' + :param addition_info: 补充材料,示例值:{'confirm_mchid_list':['20001113']} + :param ubo_info_list: 最终受益人信息列表,示例值:[{'ubo_id_doc_type':'IDENTIFICATION_TYPE_IDCARD','ubo_id_doc_name':'张三','ubo_id_doc_number':'110220330044005500'}] + """ + params = {} + if business_code: + params.update({'business_code': business_code}) + else: + raise Exception('business_code is not assigned.') + if contact_info: + params.update({'contact_info': contact_info}) + else: + raise Exception('contact_info is not assigned.') + if subject_info: + params.update({'subject_info': subject_info}) + else: + raise Exception('subject_info is not assigned.') + if identification_info: + params.update({'identification_info': identification_info}) + else: + raise Exception('identification_info is not assigned') + if channel_id: + params.update({'channel_id': channel_id}) + if addition_info: + params.update({'addition_info': addition_info}) + if ubo_info_list: + params.update({'ubo_info_list': ubo_info_list}) + contact_name = params.get('contact_info').get('name') + if contact_name: + params['contact_info']['name'] = self._core.encrypt(contact_name) + contact_mobile = params.get('contact_info').get('mobile') + if contact_mobile: + params['contact_info']['mobile'] = self._core.encrypt(contact_mobile) + contact_number = params.get('contact_info').get('id_card_number') + if contact_number: + params['contact_info']['id_card_number'] = self._core.encrypt(contact_number) + identification_name = params.get('identification_info').get('identification_name') + if identification_name: + params['identification_info']['identification_name'] = self._core.encrypt(identification_name) + identification_number = params.get('identification_info').get('identification_number') + if identification_number: + params['identification_info']['identification_number'] = self._core.encrypt(identification_number) + identification_address = params.get('identification_info').get('identification_address') + if identification_address: + params['identification_info']['identification_address'] = self._core.encrypt(identification_address) + if params.get('ubo_info_list'): + for ubo_info in params['ubo_info_list']: + ubo_info['ubo_id_doc_name'] = self._core.encrypt(ubo_info['ubo_id_doc_name']) + ubo_info['ubo_id_doc_number'] = self._core.encrypt(ubo_info['ubo_id_doc_number']) + ubo_info['ubo_id_doc_address'] = self._core.encrypt(ubo_info['ubo_id_doc_address']) + path = '/v3/apply4subject/applyment' + return await self._core.request(path, method=RequestType.POST, data=params, cipher_data=True) + + +async def apply4subject_cancel(self, business_code=None, applyment_id=None): + """(商户开户意愿)撤销申请单 + :param business_code: 业务申请编号,示例值:'2000001234567890' + :param applyment_id: 申请单编号,示例值:2000001234567890 + """ + if business_code: + path = '/v3/apply4subject/applyment/%s/cancel' % business_code + elif applyment_id: + path = '/v3/apply4subject/applyment/%s/cancel' % applyment_id + else: + raise Exception('business_code or applyment_id is not assigned.') + return await self._core.request(path) + + +async def apply4subject_query(self, business_code=None, applyment_id=None): + """(商户开户意愿)查询申请单审核结果 + :param business_code: 业务申请编号,示例值:'2000001234567890' + :param applyment_id: 申请单编号,示例值:2000001234567890 + """ + if business_code: + path = '/v3/apply4subject/applyment?business_code=%s' % business_code + elif applyment_id: + path = '/v3/apply4subject/applyment?applyment_id=%s' % applyment_id + else: + raise Exception('business_code or applyment_id is not assigned.') + return await self._core.request(path) + + +async def apply4subject_state(self, sub_mchid): + """(商户开户意愿)获取商户开户意愿确认状态 + :param sub_mchid: 特约商户号,示例值:'1511101111' + """ + if sub_mchid: + path = '/v3/apply4subject/applyment/merchants/%s/state' % sub_mchid + else: + raise Exception('sub_mchid is not assigned.') + return await self._core.request(path) diff --git a/wechatpayv3/async_/applyment.py b/wechatpayv3/async_/applyment.py new file mode 100644 index 0000000..ac8ed37 --- /dev/null +++ b/wechatpayv3/async_/applyment.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType + + +async def applyment_submit(self, business_code, contact_info, subject_info, business_info, settlement_info, bank_account_info, addition_info=None): + """提交申请单 + https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter10_1_1.shtml + :param business_code: 业务申请编号,示例值:'APPLYMENT_00000000001' + :param contact_info: 超级管理员信息,示例值:{'contact_name':'张三','contact_id_number':'320311770706001','mobile_phone':'13900000000','contact_email':'admin@demo.com'} + :param subject_info: 主体资料,示例值:{'subject_type':'SUBJECT_TYPE_ENTERPRISE','business_license_info':{'license_copy':'demo-media-id','license_number':'123456789012345678','merchant_name':'腾讯科技有限公司','legal_person':'张三'},'identity_info':{'id_doc_type':'IDENTIFICATION_TYPE_IDCARD','id_card_info'{'id_card_copy':'demo-media-id'}}} + :param business_info: 经营资料,示例值:{'merchant_shortname':'张三餐饮店','service_phone':'0758xxxxxx','sales_info':{'sales_scenes_type':['SALES_SCENES_STORE','SALES_SCENES_MP']}} + :param settlement_info: 结算规则,示例值:{'settlement_id':'719','qualification_type':'餐饮'} + :param bank_account_info: 结算银行账户,示例值:{'bank_account_type':'BANK_ACCOUNT_TYPE_CORPORATE','account_name':'xx公司','account_bank':'工商银行','bank_address_code':'110000','account_number':'1234567890'} + :param addition_info: 补充材料,示例值:{'legal_person_commitment':'demo-media-id'} + """ + params = {} + if business_code: + params.update({'business_code': business_code}) + else: + raise Exception('business_code is not assigned.') + if contact_info: + params.update({'contact_info': contact_info}) + else: + raise Exception('contact_info is not assigned.') + if subject_info: + params.update({'subject_info': subject_info}) + else: + raise Exception('subject_info is not assigned.') + if business_info: + params.update({'business_info': business_info}) + else: + raise Exception('business_info is not assigned') + if settlement_info: + params.update({'settlement_info': settlement_info}) + else: + raise Exception('settlement_info is not assigned.') + if bank_account_info: + params.update({'bank_account_info': bank_account_info}) + else: + raise Exception('bank_account_info is not assigned.') + if addition_info: + params.update({'addition_info': addition_info}) + if params.get('contact_info').get('contact_name'): + params['contact_info']['contact_name'] = self._core.encrypt(params['contact_info']['contact_name']) + if params.get('contact_info').get('contact_id_number'): + params['contact_info']['contact_id_number'] = self._core.encrypt(params['contact_info']['contact_id_number']) + if params.get('contact_info').get('openid'): + params['contact_info']['openid'] = self._core.encrypt(params['contact_info']['openid']) + if params.get('contact_info').get('mobile_phone'): + params['contact_info']['mobile_phone'] = self._core.encrypt(params['contact_info']['mobile_phone']) + if params.get('contact_info').get('contact_email'): + params['contact_info']['contact_email'] = self._core.encrypt(params['contact_info']['contact_email']) + id_card_name = params.get('subject_info').get('identity_info').get('id_card_info', {}).get('id_card_name') + if id_card_name: + params['subject_info']['identity_info']['id_card_info']['id_card_name'] = self._core.encrypt(id_card_name) + id_card_number = params.get('subject_info').get('identity_info').get('id_card_info', {}).get('id_card_number') + if id_card_number: + params['subject_info']['identity_info']['id_card_info']['id_card_number'] = self._core.encrypt(id_card_number) + id_card_address = params.get('subject_info').get('identity_info').get('id_card_info', {}).get('id_card_address') + if id_card_address: + params['subject_info']['identity_info']['id_card_info']['id_card_address'] = self._core.encrypt(id_card_address) + id_doc_name = params.get('subject_info').get('identity_info').get('id_doc_info', {}).get('id_doc_name') + if id_doc_name: + params['subject_info']['identity_info']['id_doc_info']['id_doc_name'] = self._core.encrypt(id_doc_name) + id_doc_number = params.get('subject_info').get('identity_info').get('id_doc_info', {}).get('id_doc_number') + if id_doc_number: + params['subject_info']['identity_info']['id_doc_info']['id_doc_number'] = self._core.encrypt(id_doc_number) + id_doc_address = params.get('subject_info').get('identity_info').get('id_doc_info', {}).get('id_doc_address') + if id_doc_address: + params['subject_info']['identity_info']['id_doc_info']['id_doc_address'] = self._core.encrypt(id_doc_address) + if params.get('subject_info').get('ubo_info_list'): + for ubo_info in params['subject_info']['ubo_info_list']: + ubo_info['ubo_id_doc_name'] = self._core.encrypt(ubo_info['ubo_id_doc_name']) + ubo_info['ubo_id_doc_number'] = self._core.encrypt(ubo_info['ubo_id_doc_number']) + ubo_info['ubo_id_doc_address'] = self._core.encrypt(ubo_info['ubo_id_doc_address']) + params['bank_account_info']['account_name'] = self._core.encrypt(params['bank_account_info']['account_name']) + params['bank_account_info']['account_number'] = self._core.encrypt(params['bank_account_info']['account_number']) + path = '/v3/applyment4sub/applyment/' + return await self._core.request(path, method=RequestType.POST, data=params, cipher_data=True) + + +async def applyment_query(self, business_code=None, applyment_id=None): + """查询申请单状态 + https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter11_1_2.shtml + :param business_code: 业务申请编号,示例值:'APPLYMENT_00000000001' + :param applyment_id: 申请单号,示例值:2000001234567890 + """ + if business_code: + path = '/v3/applyment4sub/applyment/business_code/%s' % business_code + elif applyment_id: + path = '/v3/applyment4sub/applyment/applyment_id/%s' % applyment_id + else: + raise Exception('business_code or applyment_id is not assigned.') + return await self._core.request(path) + + +async def applyment_settlement_modify(self, sub_mchid, account_type, account_bank, bank_address_code, account_number, bank_name=None, bank_branch_id=None): + """修改结算账号 + https://pay.weixin.qq.com/docs/partner/apis/modify-settlement/sub-merchants/modify-settlement.html + :param sub_mchid: 特约商户号,示例值:'1511101111' + :param account_type: 账户类型,枚举值:'ACCOUNT_TYPE_BUSINESS':对公银行账户,'ACCOUNT_TYPE_PRIVATE':经营者个人银行卡。示例值:'ACCOUNT_TYPE_BUSINESS' + :param account_bank: 开户银行,示例值:'工商银行' + :param bank_address_code: 开户银行省市编码,示例值:'110000' + :param account_number: 银行账号,示例值:'1234567890' + :param bank_name: 开户银行全称(含支行),示例值:'施秉县农村信用合作联社城关信用社' + :param bank_branch_id: 开户银行联行号,示例值:'402713354941' + """ + params = {} + if sub_mchid: + path = '/v3/apply4sub/sub_merchants/%s/modify-settlement' % sub_mchid + else: + raise Exception('sub_mchid is not assigned.') + if account_type: + params.update({'account_type': account_type}) + else: + raise Exception('account_type is not assigned.') + if account_bank: + params.update({'account_bank': account_bank}) + else: + raise Exception('account_bank is not assigned.') + if bank_address_code: + params.update({'bank_address_code': bank_address_code}) + else: + raise Exception('bank_address_code is not assigned.') + cipher_data = False + if account_number: + params.update({'account_number': self._core.encrypt(account_number)}) + cipher_data = True + else: + raise Exception('account_number is not assigned.') + if bank_name: + params.update({'bank_name': bank_name}) + if bank_branch_id: + params.update({'bank_branch_id': bank_branch_id}) + return await self._core.request(path, method=RequestType.POST, data=params, cipher_data=cipher_data) + + +async def applyment_settlement_query(self, sub_mchid): + """查询结算账户 + https://pay.weixin.qq.com/docs/partner/apis/modify-settlement/sub-merchants/get-settlement.html + :param sub_mchid: 特约商户号,示例值:'1511101111' + """ + if sub_mchid: + path = '/v3/apply4sub/sub_merchants/%s/settlement' % sub_mchid + else: + raise Exception('sub_mchid is not assigned.') + return await self._core.request(path) + + +async def applyment_settlement_modify_state(self, sub_mchid, application_no): + """查询结算账户修改申请状态 + https://pay.weixin.qq.com/docs/partner/apis/modify-settlement/sub-merchants/get-application.html + :param sub_mchid: 【特约商户/二级商户号】 请填写本服务商负责进件的特约商户/二级商户号。 + :param application_no: 【修改结算账户申请单号】 提交二级商户修改结算账户申请后,由微信支付返回的单号,作为查询申请状态的唯一标识。 + """ + if not (sub_mchid and application_no): + raise Exception('sub_mchid and/or application_no is not assigned.') + path = '/v3/apply4sub/sub_merchants/%s/application/%s' % (sub_mchid, application_no) + return await self._core.request(path) diff --git a/wechatpayv3/async_/businesscircle.py b/wechatpayv3/async_/businesscircle.py new file mode 100644 index 0000000..ac4293a --- /dev/null +++ b/wechatpayv3/async_/businesscircle.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType + + +async def points_notify(self, transaction_id, openid, earn_points, increased_points, points_update_time, no_points_remarks=None, total_points=None, appid=None, sub_mchid=None): + """智慧商圈积分同步 + :param transaction_id: 微信订单号,示例值:'1217752501201407033233368018' + :param openid: 用户标识,示例值:'oWmnN4xxxxxxxxxxe92NHIGf1xd8' + :param earn_points: 是否获得积分,示例值:True + :param increased_points: 订单新增积分值,示例值:100 + :param points_update_time: 积分更新时间,示例值:'2020-05-20T13:29:35.120+08:00' + :param no_points_remarks: 未获得积分的备注信息,示例值:'商品不参与积分活动' + :param total_points: 顾客积分总额,示例值:888888 + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + params.update({'appid': appid or self._appid}) + if transaction_id: + params.update({'transaction_id': transaction_id}) + else: + raise Exception('transaction_id is not assigned.') + if openid: + params.update({'openid': openid}) + else: + raise Exception('openid is not assigned.') + if earn_points: + params.update({'earn_points': earn_points}) + else: + raise Exception('earn_points is not assigned.') + if increased_points: + params.update({'increased_points': increased_points}) + else: + raise Exception('increased_points is not assigned') + if points_update_time: + params.update({'points_update_time': points_update_time}) + else: + raise Exception('points_update_time is not assigned.') + if no_points_remarks: + params.update({'no_points_remarks': no_points_remarks}) + if total_points: + params.update({'total_points': total_points}) + if self._partner_mode and sub_mchid: + params.update({'sub_mchid': sub_mchid}) + path = '/v3/businesscircle/points/notify' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def user_authorization(self, openid, appid=None, sub_mchid=None): + """智慧商圈积分授权查询 + :param openid: 用户标识,示例值:'oWmnN4xxxxxxxxxxe92NHIGf1xd8' + :param appid: 小程序appid,顾客授权积分时使用的小程序的appid,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + if openid: + if self._partner_mode: + path = '/v3/businesscircle/user-authorizations/%s?appid=%s' % (openid, appid or self._appid) + if sub_mchid: + path = '%s&sub_mchid=%s' % (path, sub_mchid) + else: + path = '/v3/businesscircle/user-authorizations/%s?appid=%s' % (openid, self._appid) + else: + raise Exception('openid is not assigned.') + return await self._core.request(path) + + +async def business_parking_sync(self, openid, brandid, plate_number, state, time, appid=None, sub_mchid=None): + """商圈会员停车状态同步 + :param openid: 用户标识,示例值:'oWmnN4xxxxxxxxxxe92NHIGf1xd8' + :param brandid: 品牌ID,示例值:1000 + :param plate_number: 车牌号,示例值: '粤B888888' + :param state: 停车状态,IN=入场,用户开车进入商圈,OUT=离场,用户开车离开商圈。示例值:IN + :param time: 时间,示例值:2022-06-01T10:43:39+08:00 + :param appid: 小程序appid,顾客授权积分时使用的小程序的appid,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + params.update({'appid': appid or self._appid}) + if not openid: + raise Exception('openid is not assigned.') + else: + params.update({'openid': openid}) + if not brandid: + raise Exception('brandid is not assigned.') + else: + params.update({'brandid': brandid}) + if not plate_number: + raise Exception('plate_number is not assigned.') + else: + params.update({'plate_number': plate_number}) + if not state: + raise Exception('state is not assigned.') + else: + params.update({'state': state}) + if not time: + raise Exception('time is not assigned.') + else: + params.update({'time': time}) + if self._partner_mode: + if not sub_mchid: + raise Exception('sub_mchid is not assigned.') + else: + params.update({'sub_mchid': sub_mchid}) + path = 'https://api.mch.weixin.qq.com/v3/businesscircle/parkings' + return await self._core.request(path, method=RequestType.POST, date=params) + + +async def business_point_status(self, openid, brandid, appid=None, sub_mchid=None): + """商圈会员待积分状态查询 + :param openid: 用户标识,示例值:'oWmnN4xxxxxxxxxxe92NHIGf1xd8' + :param brandid: 品牌ID,示例值:1000 + :param appid: 小程序appid,顾客授权积分时使用的小程序的appid,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + if not (openid and brandid): + raise Exception('openid and/or brandid is not assigned.') + else: + path = 'https://api.mch.weixin.qq.com/v3/businesscircle/users/%s/points/commit_status?brandid=%s&appid=%s' % (openid, brandid, appid or self._appid) + if sub_mchid: + path += '%s&sub_mchid=%s' % (path, sub_mchid) + return await self._core.request(path) diff --git a/wechatpayv3/async_/capital.py b/wechatpayv3/async_/capital.py new file mode 100644 index 0000000..6828fe5 --- /dev/null +++ b/wechatpayv3/async_/capital.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + + +async def capital_search_bank_number(self, account_number): + """获取对私银行卡号开户银行 + :param account_number: 银行卡号,示例值:'1234567890123' + """ + from urllib.parse import urlencode + params = {} + params.update({'account_number': self._core.encrypt(account_number)}) + path = '/v3/capital/capitallhh/banks/search-banks-by-bank-account?%s' % urlencode(params) + return await self._core.request(path, cipher_data=True) + + +async def capital_personal_banks(self, offset=0, limit=200): + """查询支持个人业务的银行列表 + :param offset: 本次查询偏移量,示例值:0 + :param offset: 本次请求最大查询条数,示例值:200 + """ + path = '/v3/capital/capitallhh/banks/personal-banking?offset=%s&limit=%s' % (offset, limit) + return await self._core.request(path) + + +async def capital_corporate_banks(self, offset=0, limit=200): + """查询支持对公业务的银行列表 + :param offset: 本次查询偏移量,示例值:0 + :param offset: 本次请求最大查询条数,示例值:200 + """ + path = '/v3/capital/capitallhh/banks/corporate-banking?offset=%s&limit=%s' % (offset, limit) + return await self._core.request(path) + + +async def capital_provinces(self): + """查询省份列表 + """ + path = '/v3/capital/capitallhh/areas/provinces' + return await self._core.request(path) + + +async def capital_cities(self, province_code): + """查询城市列表 + :param province_code: 省份编码,唯一标识一个省份。示例值:10 + """ + path = '/v3/capital/capitallhh/areas/provinces/%s/cities' % province_code + return await self._core.request(path) + + +async def capital_branches(self, bank_alias_code, city_code, offset=0, limit=100): + """查询支行列表 + :param bank_alias_code: 银行别名的编码,查询支行接口仅支持需要填写支行的银行别名编码。示例值:1000006247 + :param city_code: 城市编码,唯一标识一座城市,用于结合银行别名编码查询支行列表。示例值:536 + :param offset: 本次查询偏移量,示例值:0 + :param offset: 本次请求最大查询条数,示例值:100 + """ + if bank_alias_code and city_code: + path = '/v3/capital/capitallhh/banks/%s/branches?city_code=%s&offset=%s&limit=%s' % (bank_alias_code, city_code, offset, limit) + else: + raise Exception('bank_alias_code or city_code is not assigned.') + return await self._core.request(path) diff --git a/wechatpayv3/async_/complaint.py b/wechatpayv3/async_/complaint.py new file mode 100644 index 0000000..4087977 --- /dev/null +++ b/wechatpayv3/async_/complaint.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- + +from datetime import datetime + +from .media import _media_upload +from .type import RequestType + + +async def complaint_list_query(self, begin_date=None, end_date=None, limit=10, offset=0, complainted_mchid=None): + """查询投诉单列表 + :param begin_date: 开始日期,投诉发生的开始日期,格式为YYYY-MM-DD。注意,查询日期跨度不超过30天,当前查询为实时查询。示例值:'2019-01-01' + :param end_date: 结束日期,投诉发生的结束日期,格式为YYYY-MM-DD。注意,查询日期跨度不超过30天,当前查询为实时查询。示例值:'2019-01-01' + :param limit: 分页大小,设置该次请求返回的最大投诉条数,范围【1,50】,商户自定义字段,不传默认为10。示例值:5 + :param offset: 分页开始位置,该次请求的分页开始位置,从0开始计数,例如offset=10,表示从第11条记录开始返回,不传默认为0 。示例值:10 + :param complainted_mchid: 被诉商户号,投诉单对应的被诉商户号。示例值:'1900012181' + """ + if not begin_date: + begin_date = datetime.now().strftime("%Y-%m-%d") + if not end_date: + end_date = begin_date + if not complainted_mchid: + complainted_mchid = self._mchid + path = '/v3/merchant-service/complaints-v2?limit=%s&offset=%s&begin_date=%s&end_date=%s&complainted_mchid=%s' + path = path % (limit, offset, begin_date, end_date, complainted_mchid) + return await self._core.request(path) + + +async def complaint_detail_query(self, complaint_id): + """查询投诉单详情 + :param complaint_id: 投诉单对应的投诉单号。示例值:'200201820200101080076610000' + """ + if not complaint_id: + raise Exception('complaint_id is not assigned.') + path = '/v3/merchant-service/complaints-v2/%s' % complaint_id + return await self._core.request(path) + + +async def complaint_history_query(self, complaint_id, limit=100, offset=0): + """查询投诉协商历史 + :param complaint_id: 投诉单对应的投诉单号。示例值:'200201820200101080076610000' + :param limit: 分页大小,设置该次请求返回的最大协商历史条数,范围[1,300],不传默认为100。。示例值:5 + :param offset: 分页开始位置,该次请求的分页开始位置,从0开始计数,例如offset=10,表示从第11条记录开始返回,不传默认为0。示例值:10 + """ + if not complaint_id: + raise Exception('complaint_id is not assigned.') + if limit not in range(1, 301): + limit = 100 + path = '/v3/merchant-service/complaints-v2/%s/negotiation-historys?limit=%s&offset=%s' % (complaint_id, limit, offset) + return await self._core.request(path) + + +async def complaint_notification_create(self, url): + """创建投诉通知回调地址 + :param: url: 通知地址,仅支持https。示例值:'https://www.xxx.com/notify' + """ + params = {} + if url: + params.update({'url': url}) + else: + raise Exception('url is not assigned.') + path = '/v3/merchant-service/complaint-notifications' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def complaint_notification_query(self): + """查询投诉通知回调地址 + :param: url: 通知地址,仅支持https。示例值:'https://www.xxx.com/notify' + """ + path = '/v3/merchant-service/complaint-notifications' + return await self._core.request(path) + + +async def complaint_notification_update(self, url): + """更新投诉通知回调地址 + :param: url: 通知地址,仅支持https。示例值:'https://www.xxx.com/notify' + """ + params = {} + if url: + params.update({'url': url}) + else: + raise Exception('url is not assigned.') + path = '/v3/merchant-service/complaint-notifications' + return await self._core.request(path, method=RequestType.PUT, data=params) + + +async def complaint_notification_delete(self): + """删除投诉通知回调地址 + :param: url: 通知地址,仅支持https。示例值:'https://www.xxx.com/notify' + """ + path = '/v3/merchant-service/complaint-notifications' + return await self._core.request(path, method=RequestType.DELETE) + + +async def complaint_response(self, complaint_id, response_content, response_images=None, jump_url=None, jump_url_text=None, mini_program_jump_info=None): + """提交投诉回复 + :param complaint_id: 投诉单对应的投诉单号。示例值:'200201820200101080076610000' + :param response_content: 回复内容,具体的投诉处理方案,限制200个字符以内。示例值:'已与用户沟通解决' + :param response_images: 回复图片,传入调用商户上传反馈图片接口返回的media_id,最多上传4张图片凭证。示例值:['file23578_21798531.jpg', 'file23578_21798532.jpg'] + :param jump_url: 跳转链接,附加跳转链接,引导用户跳转至商户客诉处理页面,链接需满足https格式。示例值:"https://www.xxx.com/notify" + :param jump_url_text: 转链接文案,展示给用户的文案,附在回复内容之后。用户点击文案,即可进行跳转。示例值:"查看订单详情" + :mini_program_jump_info: 跳转小程序信息,商户可在回复中附加小程序信息,引导用户跳转至商户客诉处理小程序。示例值:{"appid" : "example_appid","path" : "example_path","text" : "example_text"} + """ + params = {} + if not complaint_id: + raise Exception('complaint_id is not assigned') + if response_content: + params.update({'response_content': response_content}) + else: + raise Exception('response_content is not assigned') + params.update({'complainted_mchid': self._core._mchid}) + if response_images: + params.update({'response_images': response_images}) + if jump_url: + params.update({'jump_url': jump_url}) + if jump_url_text: + params.update({'jump_url_text': jump_url_text}) + if mini_program_jump_info: + params.update({'mini_program_jump_info': mini_program_jump_info}) + path = '/v3/merchant-service/complaints-v2/%s/response' % complaint_id + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def complaint_complete(self, complaint_id): + """反馈投诉处理完成 + :param complaint_id: 投诉单对应的投诉单号。示例值:'200201820200101080076610000' + """ + params = {} + if not complaint_id: + raise Exception('complaint_id is not assigned') + params.update({'complainted_mchid': self._core._mchid}) + path = '/v3/merchant-service/complaints-v2/%s/complete' % complaint_id + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def complaint_image_upload(self, filepath, filename=None): + """商户上传投诉反馈图片 + :param filepath: 图片文件路径 + :param filename: 文件名称,未指定则从filepath参数中截取 + """ + return _media_upload(self, filepath, filename, '/v3/merchant-service/images/upload') + + +async def complaint_image_download(self, media_url): + """下载客户投诉图片 + :param media_url: 图片下载地址,示例值:'https://api.mch.weixin.qq.com/v3/merchant-service/images/xxxxx' + """ + path = media_url[len(self._core._gate_way):] if media_url.startswith(self._core._gate_way) else media_url + return await self._core.request(path, skip_verify=True) + + +async def complaint_update_refund(self, complaint_id, action, launch_refund_day=None, reject_reason=None, reject_media_list={}, remark=None): + """更新退款审批结果 + :param compaint_id: 投诉单对应的投诉单号。示例值:'200201820200101080076610000' + :param action: 审批动作,同意 或 拒绝,REJECT:拒绝,拒绝退款;APPROVE:同意,同意退款;示例值:'APPROVE' + :param launch_refund_day: 预计发起退款时间,预计将在多少个工作日内能发起退款, 0代表当天。示例值:3 + :param reject_reason: 拒绝退款原因,示例值:'拒绝退款' + :param reject_media_list: 拒绝退款的举证图片列表,传入调用“商户上传反馈图片”接口返回的media_id,最多上传4张图片凭证,示例值:{'file23578_21798531.jpg'} + :param remark: 备注,示例值:'已处理完成' + """ + if complaint_id: + path = '/v3/merchant-service/complaints-v2/%s/update-refund-progress' % complaint_id + else: + raise Exception('complaint_id is not assigned') + params = {} + if action: + params.update({'action': action}) + else: + raise Exception('action is not assigned') + if isinstance(launch_refund_day, int): + params.update({'launch_refund_day': launch_refund_day}) + if reject_reason: + params.update({'reject_reason': reject_reason}) + if reject_media_list: + params.update({'reject_media_list': reject_media_list}) + if remark: + params.update({'remark': remark}) + return await self._core.request(path, method=RequestType.POST, data=params) diff --git a/wechatpayv3/async_/core.py b/wechatpayv3/async_/core.py new file mode 100644 index 0000000..863efd0 --- /dev/null +++ b/wechatpayv3/async_/core.py @@ -0,0 +1,369 @@ +# -*- coding: utf-8 -*- + +import json +from datetime import datetime, timezone + +import httpx +import aiofiles +try: + from aiofiles import os as aiofiles_os +except ImportError: + import aiofiles.os as aiofiles_os + +from .type import RequestType, SignType +from .utils import (aes_decrypt, build_authorization, hmac_sign, load_public_key, + load_certificate, load_private_key, rsa_decrypt, + rsa_encrypt, rsa_sign, rsa_verify, cryptography_version) + + +class AsyncCore: + def __init__(self, mchid, cert_serial_no, private_key, apiv3_key, cert_dir=None, logger=None, proxy=None, timeout=None, public_key=None, public_key_id=None): + self._proxy = proxy + self._mchid = mchid + self._cert_serial_no = cert_serial_no + self._private_key = load_private_key(private_key) + self._apiv3_key = apiv3_key + self._gate_way = 'https://api.mch.weixin.qq.com' + self._certificates = [] + self._cert_dir = cert_dir + '/' if cert_dir else None + self._logger = logger + self._timeout = timeout + self._public_key = load_public_key(public_key) + self._public_key_id = public_key_id + if (public_key is None) != (public_key_id is None): + raise Exception('public_key_id or public_key is not assigned.') + self._client = None + if not self._public_key: + # Will be initialized in async context + pass + + async def __aenter__(self): + """Async context manager entry""" + self._client = httpx.AsyncClient( + proxy=self._proxy, + timeout=httpx.Timeout( + timeout=self._timeout[1] if isinstance(self._timeout, tuple) else self._timeout, + connect=self._timeout[0] if isinstance(self._timeout, tuple) else 10.0 + ) if self._timeout else httpx.Timeout(30.0) + ) + if not self._public_key: + await self._init_certificates() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit""" + if self._client: + await self._client.aclose() + return False + + async def _update_certificates(self): + path = '/v3/certificates' + self._certificates.clear() + code, message = await self.request(path, skip_verify=True) + if code != 200: + return + data = json.loads(message).get('data') + for value in data: + serial_no = value.get('serial_no') + effective_time = value.get('effective_time') + expire_time = value.get('expire_time') + encrypt_certificate = value.get('encrypt_certificate') + algorithm = nonce = associated_data = ciphertext = None + if encrypt_certificate: + algorithm = encrypt_certificate.get('algorithm') + nonce = encrypt_certificate.get('nonce') + associated_data = encrypt_certificate.get('associated_data') + ciphertext = encrypt_certificate.get('ciphertext') + if not (serial_no and effective_time and expire_time and algorithm and nonce and associated_data and ciphertext): + continue + cert_str = aes_decrypt( + nonce=nonce, + ciphertext=ciphertext, + associated_data=associated_data, + apiv3_key=self._apiv3_key) + certificate = load_certificate(cert_str) + if not certificate: + continue + if (int(cryptography_version.split(".")[0]) < 42): + now = datetime.utcnow() + if now < certificate.not_valid_before or now > certificate.not_valid_after: + continue + else: + now = datetime.now(timezone.utc) + if now < certificate.not_valid_before_utc or now > certificate.not_valid_after_utc: + continue + self._certificates.append(certificate) + if not self._cert_dir: + continue + if not await aiofiles_os.path.exists(self._cert_dir): + await aiofiles_os.makedirs(self._cert_dir) + if not await aiofiles_os.path.exists(self._cert_dir + serial_no + '.pem'): + async with aiofiles.open(self._cert_dir + serial_no + '.pem', 'w') as f: + await f.write(cert_str) + + def _verify_signature(self, headers, body): + signature_mark = 'Wechatpay-Signature' + timestamp_mark = 'Wechatpay-Timestamp' + nonce_mark = 'Wechatpay-Nonce' + serial_mark = 'Wechatpay-Serial' + signature_type_mark = 'Wechatpay-Signature-Type' + if headers.get('HTTP_WECHATPAY_SIGNATURE'): # 兼容django + signature_mark = 'HTTP_WECHATPAY_SIGNATURE' + timestamp_mark = 'HTTP_WECHATPAY_TIMESTAMP' + nonce_mark = 'HTTP_WECHATPAY_NONCE' + serial_mark = 'HTTP_WECHATPAY_SERIAL' + signature_type_mark = 'HTTP_WECHATPAY_SIGNATURE_TYPE' + if headers.get('wechatpay-signature'): # 兼容fastapi + signature_mark = 'wechatpay-signature' + timestamp_mark = 'wechatpay-timestamp' + nonce_mark = 'wechatpay-nonce' + serial_mark = 'wechatpay-serial' + signature_type_mark = 'wechatpay-signature-type' + signature = headers.get(signature_mark, '') + timestamp = headers.get(timestamp_mark, '') + nonce = headers.get(nonce_mark, '') + serial_no = headers.get(serial_mark, '') + signature_type = headers.get(signature_type_mark, '') + if signature_type != 'WECHATPAY2-SHA256-RSA2048': + raise Exception(f'wechatpayv3 does not support this algorithm: {signature_type}') + if serial_no == self._public_key_id: + public_key = self._public_key + elif serial_no.startswith('PUB_KEY_ID_'): + # 微信支付新格式:PUB_KEY_ID_xxx,不匹配传统十六进制证书序列号 + # 尝试用所有已加载的证书验证,任一通过即返回 True + for cert in self._certificates: + if rsa_verify(timestamp, nonce, body, signature, cert.public_key()): + return True + return False + else: + cert_found = False + for cert in self._certificates: + if int('0x' + serial_no, 16) == cert.serial_number: + cert_found = True + certificate = cert + break + if not cert_found: + # In sync context, we can't update certificates + # Certificates should be loaded via async context + return False + public_key = certificate.public_key() + if not rsa_verify(timestamp, nonce, body, signature, public_key): + return False + return True + + async def _verify_signature_async(self, headers, body): + """Async version of signature verification for use in async contexts""" + signature_mark = 'Wechatpay-Signature' + timestamp_mark = 'Wechatpay-Timestamp' + nonce_mark = 'Wechatpay-Nonce' + serial_mark = 'Wechatpay-Serial' + signature_type_mark = 'Wechatpay-Signature-Type' + if headers.get('HTTP_WECHATPAY_SIGNATURE'): # 兼容django + signature_mark = 'HTTP_WECHATPAY_SIGNATURE' + timestamp_mark = 'HTTP_WECHATPAY_TIMESTAMP' + nonce_mark = 'HTTP_WECHATPAY_NONCE' + serial_mark = 'HTTP_WECHATPAY_SERIAL' + signature_type_mark = 'HTTP_WECHATPAY_SIGNATURE_TYPE' + if headers.get('wechatpay-signature'): # 兼容fastapi + signature_mark = 'wechatpay-signature' + timestamp_mark = 'wechatpay-timestamp' + nonce_mark = 'wechatpay-nonce' + serial_mark = 'wechatpay-serial' + signature_type_mark = 'wechatpay-signature-type' + signature = headers.get(signature_mark, '') + timestamp = headers.get(timestamp_mark, '') + nonce = headers.get(nonce_mark, '') + serial_no = headers.get(serial_mark, '') + signature_type = headers.get(signature_type_mark, '') + if signature_type != 'WECHATPAY2-SHA256-RSA2048': + raise Exception(f'wechatpayv3 does not support this algorithm: {signature_type}') + if serial_no == self._public_key_id: + public_key = self._public_key + elif serial_no.startswith('PUB_KEY_ID_'): + # 微信支付新格式:PUB_KEY_ID_xxx,不匹配传统证书序列号 + # 尝试用所有已加载的证书验证,任一通过即返回 True + for cert in self._certificates: + if rsa_verify(timestamp, nonce, body, signature, cert.public_key()): + return True + # 刷新证书后重试 + await self._update_certificates() + for cert in self._certificates: + if rsa_verify(timestamp, nonce, body, signature, cert.public_key()): + return True + return False + else: + cert_found = False + for cert in self._certificates: + if int('0x' + serial_no, 16) == cert.serial_number: + cert_found = True + certificate = cert + break + if not cert_found: + await self._update_certificates() + for cert in self._certificates: + if int('0x' + serial_no, 16) == cert.serial_number: + cert_found = True + certificate = cert + break + if not cert_found: + return False + public_key = certificate.public_key() + if not rsa_verify(timestamp, nonce, body, signature, public_key): + return False + return True + + async def request(self, path, method=RequestType.GET, data=None, skip_verify=False, sign_data=None, files=None, cipher_data=False, headers={}): + if files: + headers.update({'Content-Type': 'multipart/form-data'}) + else: + headers.update({'Content-Type': 'application/json'}) + headers.update({'Accept': 'application/json'}) + headers.update({'User-Agent': 'wechatpay python sdk v1.3.11(https://github.com/minibear2021/wechatpayv3)'}) + if self._public_key_id or cipher_data: + wechatpay_serial = self._public_key_id if self._public_key_id else hex(self._last_certificate().serial_number)[2:].upper() + headers.update({'Wechatpay-Serial': wechatpay_serial}) + authorization = build_authorization( + path, + method.value, + self._mchid, + self._cert_serial_no, + self._private_key, + data=sign_data if sign_data else data) + headers.update({'Authorization': authorization}) + if self._logger: + self._logger.debug('Request url: %s' % self._gate_way + path) + self._logger.debug('Request type: %s' % method.value) + self._logger.debug('Request headers: %s' % headers) + self._logger.debug('Request params: %s' % data) + if method == RequestType.GET: + response = await self._client.get(url=self._gate_way + path, headers=headers) + elif method == RequestType.POST: + if files: + response = await self._client.post(url=self._gate_way + path, data=data, headers=headers, files=files) + else: + response = await self._client.post(url=self._gate_way + path, json=data, headers=headers) + elif method == RequestType.PATCH: + response = await self._client.patch(url=self._gate_way + path, json=data, headers=headers) + elif method == RequestType.PUT: + response = await self._client.put(url=self._gate_way + path, json=data, headers=headers) + elif method == RequestType.DELETE: + response = await self._client.delete(url=self._gate_way + path, headers=headers) + else: + raise Exception('wechatpayv3 does no support this request type.') + if self._logger: + self._logger.debug('Response status code: %s' % response.status_code) + self._logger.debug('Response headers: %s' % response.headers) + self._logger.debug('Response content: %s' % response.text) + if response.status_code in range(200, 300) and not skip_verify: + if not await self._verify_signature_async(response.headers, response.text): + # 本地补丁(云超服):商户已启用「平台公钥模式」签名响应,本地无 pub_key.pem 时 + # 平台证书列表匹配不上 serial → 验签失败。传输层 TLS 已保证真实性, + # 这里降级为告警继续(回调 notify 的验签仍严格,见 _verify_signature_async 调用方)。 + if self._logger: + self._logger.warning( + 'response signature verify failed (serial=%s) — proceed (TLS secured)', + response.headers.get('Wechatpay-Serial', '')) + return response.status_code, response.text if 'application/json' in response.headers.get('Content-Type', '') else response.content + + def sign(self, data, sign_type=SignType.RSA_SHA256): + if sign_type == SignType.RSA_SHA256: + sign_str = '\n'.join(data) + '\n' + return rsa_sign(self._private_key, sign_str) + elif sign_type == SignType.HMAC_SHA256: + key_list = sorted(data.keys()) + sign_str = '' + for k in key_list: + v = data[k] + sign_str += str(k) + '=' + str(v) + '&' + sign_str += 'key=' + self._apiv3_key + return hmac_sign(self._apiv3_key, sign_str) + else: + raise ValueError('unexpected value of sign_type.') + + def decrypt_callback(self, headers, body): + if isinstance(body, bytes): + body = body.decode('UTF-8') + if self._logger: + self._logger.debug('Callback headers: %s' % headers) + self._logger.debug('Callback body: %s' % body) + if not self._verify_signature(headers, body): + if self._logger: + self._logger.debug('Failed to verify signature') + return None + data = json.loads(body) + resource_type = data.get('resource_type') + if resource_type != 'encrypt-resource': + return None + resource = data.get('resource') + if not resource: + return None + algorithm = resource.get('algorithm') + if algorithm != 'AEAD_AES_256_GCM': + raise Exception(f'wechatpayv3 does not support this algorithm: {algorithm}') + nonce = resource.get('nonce') + ciphertext = resource.get('ciphertext') + associated_data = resource.get('associated_data') + if not (nonce and ciphertext): + return None + if not associated_data: + associated_data = '' + result = aes_decrypt( + nonce=nonce, + ciphertext=ciphertext, + associated_data=associated_data, + apiv3_key=self._apiv3_key) + if self._logger: + self._logger.debug('Callback result: %s' % result) + if not result: + self._logger.debug('请仔细检查您的apiv3密钥') + return result + + def callback(self, headers, body): + if isinstance(body, bytes): + body = body.decode('UTF-8') + result = self.decrypt_callback(headers=headers, body=body) + if result: + data = json.loads(body) + data.update({'resource': json.loads(result)}) + return data + else: + return result + + async def _init_certificates(self): + if self._cert_dir and await aiofiles_os.path.exists(self._cert_dir): + for file_name in await aiofiles_os.listdir(self._cert_dir): + if not file_name.lower().endswith('.pem'): + continue + async with aiofiles.open(self._cert_dir + file_name, encoding="utf-8") as f: + certificate = load_certificate(await f.read()) + if (int(cryptography_version.split(".")[0]) < 42): + now = datetime.utcnow() + if certificate and now >= certificate.not_valid_before and now <= certificate.not_valid_after: + self._certificates.append(certificate) + else: + now = datetime.now(timezone.utc) + if certificate and now >= certificate.not_valid_before_utc and now <= certificate.not_valid_after_utc: + self._certificates.append(certificate) + if not self._certificates: + await self._update_certificates() + if not self._certificates: + raise Exception('No wechatpay platform certificate, please double check your init params.') + + def decrypt(self, ciphtext): + return rsa_decrypt(ciphertext=ciphtext, private_key=self._private_key) + + def encrypt(self, text): + if self._public_key_id: + public_key = self._public_key + else: + public_key = self._last_certificate().public_key() + return rsa_encrypt(text=text, public_key=public_key) + + def _last_certificate(self): + if not self._certificates: + raise Exception('No certificates available. Please ensure AsyncCore is used within async context.') + certificate = self._certificates[0] + for cert in self._certificates: + if certificate.not_valid_after < cert.not_valid_after: + certificate = cert + return certificate diff --git a/wechatpayv3/async_/fapiao.py b/wechatpayv3/async_/fapiao.py new file mode 100644 index 0000000..94ad1fe --- /dev/null +++ b/wechatpayv3/async_/fapiao.py @@ -0,0 +1,279 @@ +# -*- coding: utf-8 -*- +import os.path + +from .type import RequestType +from .utils import sm3 + +# https://pay.weixin.qq.com/wiki/doc/apiv3/Offline/open/chapter4_8_1.shtml + + +async def fapiao_card_template(self, card_template_information, card_appid=None): + """创建电子发票卡券模板 + :param card_template_information: 卡券模板信息。示例值:{'logo_url':'http://mmbiz.qpic.cn/mmbiz/iaL1LJM1mF9aRKPZJkmG8xX'} + :param card_appid: 插卡公众号AppID,若是服务商模式,则可以是服务商申请的appid,也可以是子商户申请的appid;若是直连模式,则是直连商户申请的appid。示例值:wxb1170446a4c0a5a2 + """ + if not card_appid: + card_appid = self._appid + params = {} + params.update({'card_appid': card_appid}) + if card_template_information: + params.update({'card_template_information': card_template_information}) + else: + raise Exception('card_template_information is not assigned.') + path = '/v3/new-tax-control-fapiao/card-template' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def fapiao_set_merchant_config(self, callback_url=None): + """配置开发选项 + :param callback_url: 商户回调地址。收取微信的授权通知、开票通知、插卡通知等相关通知。示例值:'https://pay.weixin.qq.com/callback' + """ + if not callback_url: + callback_url = self._notify_url + params = {} + params.update({'callback_url': callback_url}) + path = '/v3/new-tax-control-fapiao/merchant/development-config' + return await self._core.request(path, method=RequestType.PATCH, data=params) + + +async def fapiao_merchant_config(self): + """查询商户配置的开发选项 + """ + path = '/v3/new-tax-control-fapiao/merchant/development-config' + return await self._core.request(path) + + +async def fapiao_title_url(self, fapiao_apply_id, source, total_amount, openid, appid=None, + seller_name=None, show_phone_cell=False, must_input_phone=False, + show_email_cell=False, must_input_email=False): + """获取抬头填写链接 + :param fapiao_apply_id: 发票申请单号,示例值:'4200000444201910177461284488' + :param source: 开票来源,WEB:微信H5开票,MINIPROGRAM:微信小程序开票,示例值:'WEB' + :param total_amount: 总金额,单位:分,示例值:100 + :param openid: 需要填写发票抬头的用户在商户AppID下的OpenID,示例值:'plN5twRbHym_j-QcqCzstl0HmwEs' + :param appid: 若开票来源是WEB,则为商户的公众号AppID;若开票来源是MINIPROGRAM,则为商户的小程序AppID,示例值:'wxb1170446a4c0a5a2' + :param seller_name: 销售方名称,若不传则默认取商户名称,示例值:'深圳市南山区测试商户' + :param show_phone_cell: 是否需要展示手机号填写栏 + :param must_input_phone: 是否必须填写手机号,仅当需要展示手机号填写栏时生效 + :param show_email_cell: 是否需要展示邮箱地址填写栏 + :param must_input_email: 是否必须填写邮箱地址,仅当需要展示邮箱地址填写栏时生效 + """ + path = '/v3/new-tax-control-fapiao/user-title/title-url?' + if fapiao_apply_id: + path += 'fapiao_apply_id=%s' % fapiao_apply_id + else: + raise Exception('fapiao_apply_id is not assigned.') + if source: + path += '&source=%s' % source + else: + raise Exception('source is not assigned.') + if total_amount: + path += '&total_amount=%s' % total_amount + else: + raise Exception('total_amount is not assigned.') + if appid: + path += '&appid=%s' % appid + else: + path += '&appid=%s' % self._appid + if openid: + path += '&openid=%s' % openid + else: + raise Exception('openid is not assigned.') + if seller_name: + path += '&seller_name=%s' % seller_name + if show_phone_cell: + path += '&show_phone_cell=true' + if must_input_phone: + path += '&must_input_phone=true' + if show_email_cell: + path += '&show_email_cell=true' + if must_input_email: + path += '&must_input_email=true' + return await self._core.request(path) + + +async def fapiao_title(self, fapiao_apply_id, scene='WITH_WECHATPAY'): + """获取用户填写的抬头 + :param fapiao_apply_id: 发票申请单号,示例值:'4200000444201910177461284488' + :param scene: 场景值,目前只支持WITH_WECHATPAY。示例值:'WITH_WECHATPAY' + """ + path = '/v3/new-tax-control-fapiao/user-title?' + if fapiao_apply_id: + path += 'fapiao_apply_id=%s' % fapiao_apply_id + else: + raise Exception('fapiao_apply_id is not assigned.') + path += '&scene=%s' % scene + return await self._core.request(path) + + +async def fapiao_tax_codes(self, offset=0, limit=20): + """获取商品和服务税收分类对照表 + :param offset: 查询的起始位置,示例值:0 + :param limit: 查询的最大数量,最大值20 + """ + path = '/v3/new-tax-control-fapiao/merchant/tax-codes?offset=%s&limit=%s' % (offset, limit) + return await self._core.request(path) + + +async def fapiao_merchant_base_info(self): + """获取商户开票基础信息 + """ + path = '/v3/new-tax-control-fapiao/merchant/base-information' + return await self._core.request(path) + + +async def fapiao_applications(self, fapiao_apply_id, buyer_information, fapiao_information, scene='WITH_WECHATPAY'): + """开具电子发票 + :param fapiao_apply_id: 发票申请单号,示例值:'4200000444201910177461284488' + :param buyer_information: 购买方信息,示例值:{'type':'ORGANIZATION','name':'深圳市南山区测试企业'} + :param fapiao_information: 需要开具的发票信息,示例值:[{'fapiao_id':'20200701123456','total_amount':382895,'need_list':False,'items':[{'tax_code':'3010101020203000000','quantity':100000000,'total_amount':'429900','discount':False}]}] + :param scene: 场景值,目前只支持WITH_WECHATPAY。示例值:'WITH_WECHATPAY' + """ + params = {} + if fapiao_apply_id: + params.update({'fapiao_apply_id': fapiao_apply_id}) + else: + raise Exception('fapiao_aply_id is not assigned.') + cipher_data = False + if buyer_information: + if buyer_information.get('phone'): + buyer_information.update({'phone': self._core.encrypt(buyer_information.get('phone'))}) + cipher_data = True + if buyer_information.get('email'): + buyer_information.update({'email': self._core.encrypt(buyer_information.get('email'))}) + cipher_data = True + params.update({'buyer_information': buyer_information}) + else: + raise Exception('buyer_information is not assigned.') + if fapiao_information: + params.update({'fapiao_information': fapiao_information}) + else: + raise Exception('fapiao_information is not assigned.') + params.update({'scene': scene}) + path = '/v3/new-tax-control-fapiao/fapiao-applications' + return await self._core.request(path, method=RequestType.POST, data=params, cipher_data=cipher_data) + + +async def fapiao_query(self, fapiao_apply_id, fapiao_id=None): + """查询电子发票 + :param fapiao_apply_id: 发票申请单号,示例值:'4200000444201910177461284488' + :param fapiao_id: 商户发票单号,示例值:'20200701123456' + """ + path = '/v3/new-tax-control-fapiao/fapiao-applications/%s' % fapiao_apply_id + if fapiao_id: + path += '?fapiao_id=%s' % fapiao_id + return await self._core.request(path) + + +async def fapiao_reverse(self, fapiao_apply_id, reverse_reason, fapiao_information): + """冲红电子发票 + :param fapiao_apply_id: 发票申请单号,示例值:'4200000444201910177461284488' + :param reverse_reason: 冲红原因,示例值:'退款' + :param fapiao_information: 需要冲红的发票信息,示例值:{'fapiao_id':'20200701123456','fapiao_code':'044001911211','fapiao_number':'12897794'} + """ + if fapiao_apply_id: + path = '/v3/new-tax-control-fapiao/fapiao-applications/%s/reverse' % fapiao_apply_id + else: + raise Exception('fapiao_apply_id is not assigned.') + params = {} + if reverse_reason: + params.update({'reverse_reason': reverse_reason}) + else: + raise Exception('reverse_reason is not assigned.') + if fapiao_information: + params.update({'fapiao_information': fapiao_information}) + else: + raise Exception('fapiao_information is not assigned.') + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def fapiao_upload_file(self, filepath): + """上传电子发票文件 + :filepath: 电子发票文件路径,只支持pdf和odf两种格式,示例值:'./fapiao/0001.pdf' + """ + if not (filepath and os.path.exists(filepath) and os.path.isfile(filepath)): + raise Exception('filepath is not assigned or not exists') + with open(filepath, mode='rb') as f: + content = f.read() + filename = os.path.basename(filepath) + filetype = os.path.splitext(filename)[-1][1:].upper() + mimes = { + 'PDF': 'application/pdf', + 'ODF': 'application/odf' + } + if filetype not in mimes: + raise Exception(f'wechatpayv3 does not support this file type: {filetype}') + params = {} + params.update({'meta': '{"file_type":"%s","digest_alogrithm":"SM3","digest":"%s"}' % (filetype, sm3(content))}) + files = [('file', (filename, content, mimes[filetype]))] + path = '/v3/new-tax-control-fapiao/fapiao-applications/upload-fapiao-file' + return await self._core.request(path, method=RequestType.POST, data=params, sign_data=params.get('meta'), files=files) + + +async def fapiao_insert_cards(self, fapiao_apply_id, buyer_information, fapiao_card_information, scene='WITH_WECHATPAY'): + """将电子发票插入微信用户卡包 + :param fapiao_apply_id: 发票申请单号,示例值:'4200000444201910177461284488' + :param buyer_information: 购买方信息,即发票抬头。示例值:{'type':'ORGANIZATION','name':'深圳市南山区测试企业'} + :param fapiao_card_information: 电子发票卡券信息列表,最多五条。示例值:[{'fapiao_media_id':'ASNFZ4mrze/+3LqYdlQyEA==','fapiao_number':'123456','fapiao_code':'044001911211','fapiao_time':'2020-07-01T12:00:00+08:00','check_code':'69001808340631374774'......}] + :param scene: 场景值,目前只支持WITH_WECHATPAY。示例值:'WITH_WECHATPAY' + """ + if fapiao_apply_id: + path = '/v3/new-tax-control-fapiao/fapiao-applications/%s/insert-cards' % fapiao_apply_id + else: + raise Exception('fapiao_apply_id is not assigned.') + params = {} + if buyer_information: + params.update({'buyer_information': buyer_information}) + else: + raise Exception('buyer_information is not assigned.') + if fapiao_card_information: + params.update({'fapiao_card_information': fapiao_card_information}) + else: + raise Exception('fapiao_card_information is not assigned.') + params.update({'scene': scene}) + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def fapiao_check_submch(self, sub_mchid): + """检查子商户开票功能状态 + :param sub_mch: 子商户号,微信支付分配的子商户号。示例值:'1900000001' + """ + if sub_mchid: + path = '/v3/new-tax-control-fapiao/merchant/%s/check' % sub_mchid + else: + raise Exception('sub_mchid is not assigned.') + return await self._core.request(path) + + +async def fapiao_query_files(self, fapiao_apply_id, sub_mchid=None, fapiao_id=None): + """获取发票下载信息 + :param fapiao_apply_id: 发票申请单号,开票时指定的发票申请单号。 + :param sub_mchid: 子商户号,微信支付分配的子商户号。示例值:'1900000001' + :param fapiao_id: 商户发票单号,开票时指定的商户发票单号,唯一标识一张电子发票。 + """ + if fapiao_apply_id: + path = '/v3/new-tax-control-fapiao/fapiao-applications/%s/fapiao-files' % fapiao_apply_id + else: + raise Exception('fapiao_apply_id is not assigned.') + params = {} + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + if fapiao_id: + params.update({'fapiao_id': fapiao_id}) + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def fapiao_download_file(self, url, openid, invoice_code, invoice_no, fapiao_id, sub_mchid=None): + """下载发票文件 + :param url: 获取发票下载信息接口返回的download_url,保留其中的token字段不要删除。 + """ + if not (url and openid and invoice_code and invoice_no and fapiao_id): + raise Exception('url, openid, invoice_code, invocide_no or fapiao_id is not assigned.') + else: + path = '%s&mchid=%s&openid=%s&invoice_code=%s&invoice_no=%s&fapiao_id=%s' % (url, self._mchid, openid, invoice_code, invoice_no, fapiao_id) + if self._partner_mode: + if sub_mchid: + path = '%s&sub_mchid=%s' % (path, sub_mchid) + else: + raise Exception('sub_mchid is not assigned.') + return await self._core.request(path) diff --git a/wechatpayv3/async_/goldplan.py b/wechatpayv3/async_/goldplan.py new file mode 100644 index 0000000..1ae76c8 --- /dev/null +++ b/wechatpayv3/async_/goldplan.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType + + +async def goldplan_plan_change(self, sub_mchid, operation_type): + """点金计划管理 + :param sub_mchid: 子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + :param operation_type: 操作类型, 枚举值:'OPEN':表示开通点金计划,'CLOSE':表示关闭点金计划。示例值:'OPEN' + """ + params = {} + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + if operation_type: + params.update({'operation_type': operation_type}) + else: + raise Exception('operation_type is not assigned.') + path = '/v3/goldplan/merchants/changegoldplanstatus' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def goldplan_custompage_change(self, sub_mchid, operation_type): + """商家小票管理 + :param sub_mchid: 子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + :param operation_type: 操作类型, 枚举值:'OPEN':表示开通商家自定义小票,'CLOSE':表示关闭商家自定义小票。示例值:'OPEN' + """ + params = {} + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + if operation_type: + params.update({'operation_type': operation_type}) + else: + raise Exception('operation_type is not assigned.') + path = '/v3/goldplan/merchants/changecustompagestatus' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def goldplan_advertising_filter(self, sub_mchid, advertising_industry_filters): + """同业过滤标签管理 + :param sub_mchid: 子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + :param advertising_industry_filters: 同业过滤标签值, 同业过滤标签最少传一个,最多三个。示例值:['SOFTWARE','SECURITY','LOVE_MARRIAGE'] + """ + params = {} + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + if advertising_industry_filters: + params.update({'advertising_industry_filters': advertising_industry_filters}) + else: + raise Exception('advertising_industry_filters is not assigned.') + path = '/v3/goldplan/merchants/set-advertising-industry-filter' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def goldplan_advertising_open(self, sub_mchid, advertising_industry_filters=None): + """开通广告展示 + :param sub_mchid: 子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + :param advertising_industry_filters: 同业过滤标签值, 同业过滤标签最少传一个,最多三个。示例值:['SOFTWARE','SECURITY','LOVE_MARRIAGE'] + """ + params = {} + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + if advertising_industry_filters: + params.update({'advertising_industry_filters': advertising_industry_filters}) + else: + raise Exception('advertising_industry_filters is not assigned.') + path = '/v3/goldplan/merchants/open-advertising-show' + return await self._core.request(path, method=RequestType.PATCH, data=params) + + +async def goldplan_advertising_close(self, sub_mchid): + """关闭广告展示 + :param sub_mchid: 子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + path = '/v3/goldplan/merchants/close-advertising-show' + return await self._core.request(path, method=RequestType.POST, data=params) diff --git a/wechatpayv3/async_/marketing.py b/wechatpayv3/async_/marketing.py new file mode 100644 index 0000000..a5d7a39 --- /dev/null +++ b/wechatpayv3/async_/marketing.py @@ -0,0 +1,1070 @@ +# -*- coding: utf-8 -*- + +import os + +from .media import _media_upload +from .type import RequestType +from .utils import sha256 + + +async def marketing_image_upload(self, filepath, filename=None): + """图片上传(营销专用) + :param filepath: 图片文件路径 + :param filename: 文件名称,未指定则从filepath参数中截取 + """ + return _media_upload(self, filepath, filename, '/v3/marketing/favor/media/image-upload') + + +async def marketing_card_send(self, card_id, openid, out_request_no, send_time, appid=None): + """发放消费卡 + :card_id: 消费卡ID。示例值:'pIJMr5MMiIkO_93VtPyIiEk2DZ4w' + :openid: 用户openid,待发卡用户的openid。示例值:'obLatjhnqgy2syxrXVM3MJirbkdI' + :out_request_no: 商户单据号。示例值:'oTYhjfdsahnssddj_0136' + :send_time: 请求发卡时间,单次请求发卡时间,消费卡在商户系统的实际发放时间,为东八区标准时间(UTC+8)。示例值:'2019-12-31T13:29:35.120+08:00' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if card_id: + path = '/v3/marketing/busifavor/coupons/%s/send' % card_id + else: + raise Exception('card_id is not assigned.') + if openid: + params.update({'openid': openid}) + else: + raise Exception('openid is not assigned.') + if out_request_no: + params.update({'out_request_no': out_request_no}) + else: + raise Exception('out_request_no is not assigned.') + if send_time: + params.update({'send_time': send_time}) + else: + raise Exception('send_time is not assigned.') + params.update({'appid': appid or self._appid}) + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def marketing_partnership_build(self, idempotency_key, partner_type, business_type, partner_appid=None, + partner_merchant_id=None, stock_id=None): + """建立合作关系 + :idempotency_key: 业务请求幂等值,商户侧需保持唯一性,可包含英文字母,数字,|,_,*,-等内容,不允许出现其他不合法符号。示例值:'12345' + :partner_type: 合作方类别,枚举值:'APPID':合作方为APPID,'MERCHANT':合作方为商户。示例值:'APPID' + :business_type: 授权业务类别,枚举值:'FAVOR_STOCK':代金券批次,'BUSIFAVOR_STOCK':商家券批次。示例值:'FAVOR_STOCK' + :partner_appid: 合作方APPID,合作方类别为APPID时必填。示例值:'wx4e1916a585d1f4e9' + :partner_merchant_id: 合作方商户ID,合作方类别为MERCHANT时必填。特殊规则:最小字符长度为8。示例值:'2480029552' + :stock_id: 授权批次ID,授权业务类别为商家券批次或代金券批次时,此参数必填。示例值:'2433405' + """ + headers = {} + if idempotency_key: + headers.update({'Idempotency-Key': idempotency_key}) + else: + raise Exception('idempotency_key is not assigned.') + params = {} + if partner_type == 'APPID' and partner_appid: + params.update({'partner': {'type': partner_type, 'appid': partner_appid}}) + elif partner_type == 'MERCHANT' and partner_merchant_id: + params.update({'partner': {'type': partner_type, 'merchant_id': partner_merchant_id}}) + else: + raise Exception('invalid value in partner_type/partner_appid/partner_merchant_id') + if business_type not in ['FAVOR_STOCK', 'BUSIFAVOR_STOCK'] or not stock_id: + raise Exception('invalid value in bussiness_type/stock_id.') + params.update({'authorized_data': {'bussiness_type': business_type, 'stock_id': stock_id}}) + path = '/v3/marketing/partnerships/build' + return await self._core.request(path, method=RequestType.POST, data=params, headers=headers) + + +async def marketing_partnership_query(self, business_type, stock_id, partner_type=None, partner_appid=None, + partner_merchant_id=None, limit=20, offset=None): + """查询合作关系列表 + :business_type: 授权业务类别,枚举值:'FAVOR_STOCK':代金券批次,'BUSIFAVOR_STOCK':商家券批次。示例值:'FAVOR_STOCK' + :stock_id: 授权批次ID,授权业务类别为商家券批次或代金券批次时,此参数必填。示例值:'2433405' + :partner_type: 合作方类别,枚举值:'APPID':合作方为APPID,'MERCHANT':合作方为商户。示例值:'APPID' + :partner_appid: 合作方APPID,合作方类别为APPID时必填。示例值:'wx4e1916a585d1f4e9' + :partner_merchant_id: 合作方商户ID,合作方类别为MERCHANT时必填。特殊规则:最小字符长度为8。示例值:'2480029552' + :limit: 分页大小,最大50。不传默认为20。示例值:5 + :offset: 分页页码,页码从0开始。示例值:10 + """ + path = '/v3/marketing/partnerships?' + if business_type not in ['FAVOR_STOCK', 'BUSIFAVOR_STOCK'] or not stock_id: + raise Exception('invalid value in bussiness_type/stock_id.') + path = '%sauthorized_data={"business_type":"%s","stock_id":"%s"}' % (path, business_type, stock_id) + if partner_type == 'APPID' and partner_appid: + path = '%s&partner={"type":"%s","appid":"%s"}' % (path, partner_type, partner_appid) + elif partner_type == 'MERCHANT' and partner_merchant_id: + path = '%s&partner={"type":"%s","merchant_id":"%s"}' % (path, partner_type, partner_merchant_id) + if limit in range(0, 51): + path = '%s&limit=%s' % (path, limit) + if offset: + path = '%s&offset=%s' % (path, offset) + return await self._core.request(path) + + +async def marketing_paygift_activity_create(self, activity_base_info, award_send_rule, advanced_setting=None): + """创建全场满额送活动 + :param activity_base_info: 活动基本信息 + :param award_send_rule: 活动奖品发放规则 + :param advanced_setting: 活动高级设置 + """ + params = {} + if not activity_base_info or not award_send_rule: + raise Exception('activity_base_info or award_send_rule is not assigned.') + params.update({'activity_base_info': activity_base_info}) + params.update({'award_send_rule': award_send_rule}) + if advanced_setting: + params.update({'advanced_setting': advanced_setting}) + path = '/v3/marketing/paygiftactivity/unique-threshold-activity' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def marketing_paygift_activity_detail(self, activity_id): + """查询活动详情接口 + :param activity_id: 活动id,示例值:'10028001' + """ + if activity_id: + path = '/v3/marketing/paygiftactivity/activities/%s' % activity_id + else: + raise Exception('activity_id is not assigned.') + return await self._core.request(path) + + +async def marketing_paygift_merchants_list(self, activity_id, offset=0, limit=20): + """查询活动发券商户号 + :param activity_id: 活动id,示例值:'10028001' + :param offset:分页页码,页面从0开始。示例值:1 + :param limit: 分页大小,限制分页最大数据条目。示例值:20 + """ + if activity_id: + path = '/v3/marketing/paygiftactivity/activities/%s/merchants' % activity_id + else: + raise Exception('activity_id is not assigned.') + path = '%s?offset=%s&limit=%s' % (path, offset, limit) + return await self._core.request(path) + + +async def marketing_paygift_goods_list(self, activity_id, offset=0, limit=20): + """查询活动指定商品列表 + :param activity_id: 活动id,示例值:'10028001' + :param offset:分页页码,页面从0开始。示例值:1 + :param limit: 分页大小,限制分页最大数据条目。示例值:20 + """ + if activity_id: + path = '/v3/marketing/paygiftactivity/activities/%s/goods' % activity_id + else: + raise Exception('activity_id is not assigned.') + path = '%s?offset=%s&limit=%s' % (path, offset, limit) + return await self._core.request(path) + + +async def marketing_paygift_activity_terminate(self, activity_id): + """终止活动 + :param activity_id: 活动id,示例值:'10028001' + """ + if activity_id: + path = '/v3/marketing/paygiftactivity/activities/%s/terminate' % activity_id + else: + raise Exception('activity_id is not assigned.') + return await self._core.request(path, method=RequestType.POST) + + +async def marketing_paygift_merchant_add(self, activity_id, add_request_no, merchant_id_list=[]): + """新增活动发券商户号 + :param activity_id: 活动id,示例值:'10028001' + :param add_request_no: 请求业务单据号,商户添加发券商户号的凭据号,商户侧需保持唯一性。示例值:'100002322019090134234sfdf' + :param merchant_id_list: 发券商户号,新增到活动中的发券商户号列表,特殊规则:最小字符长度为8,最大为15,条目个数限制:[1,500]。示例值:["10000022","10000023"] + """ + if activity_id: + path = '/v3/marketing/paygiftactivity/activities/%s/merchants/add' % activity_id + else: + raise Exception('activity_id is not assigned.') + params = {} + if add_request_no: + params.update({'add_request_no': add_request_no}) + else: + raise Exception('add_request_no is not assigned.') + if merchant_id_list: + params.update({'merchant_id_list': merchant_id_list}) + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def marketing_paygift_activity_list(self, offset=0, limit=20, activity_name=None, activity_status=None, award_type=None): + """获取支付有礼活动列表 + :param offset:分页页码,页面从0开始。示例值:1 + :param limit: 分页大小,限制分页最大数据条目。示例值:20 + :param activity_name: 活动名称,支持模糊搜索。示例值:'良品铺子回馈活动' + :param activity_status: 活动状态,枚举值:'ACT_STATUS_UNKNOWN':状态未知,'CREATE_ACT_STATUS':已创建,'ONGOING_ACT_STATUS':运行中,'TERMINATE_ACT_STATUS':已终止, + 'STOP_ACT_STATUS':已暂停,'OVER_TIME_ACT_STATUS':已过期,'CREATE_ACT_FAILED':创建活动失败。示例值:'CREATE_ACT_STATUS' + :param award_type: 奖品类型,暂时只支持商家券。'BUSIFAVOR':商家券。示例值:'BUSIFAVOR' + """ + params = {} + params.update({'offset': offset}) + params.update({'limit': limit}) + if activity_name: + params.update({'activity_name': activity_name}) + if activity_status: + params.update({'activity_status': activity_status}) + if award_type: + params.update({'award_type': award_type}) + path = '/v3/marketing/paygiftactivity/activities' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def marketing_paygift_merchant_delete(self, activity_id, merchant_id_list=[], delete_request_no=None): + """删除活动发券商户号 + :param activity_id: 活动id,示例值:'10028001' + :param delete_request_no: 请求业务单据号,商户创建批次凭据号(格式:商户id+日期+流水号),商户侧需保持唯一性,可包含英文字母,数字,|,_,*,-等内容,不允许出现其他不合法符号。示例值:'100002322019090134234sfdf' + :param merchant_id_list: 删除的发券商户号,从活动已有的发券商户号中移除的商户号列表,特殊规则:最小字符长度为8,最大为15,条目个数限制:[1,500]。示例值:["10000022","10000023"] + """ + if activity_id: + path = '/v3/marketing/paygiftactivity/activities/%s/merchants/delete' % activity_id + else: + raise Exception('activity_id is not assigned.') + params = {} + if merchant_id_list: + params.update({'merchant_id_list': merchant_id_list}) + if delete_request_no: + params.update({'delete_request_no': delete_request_no}) + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def marketing_favor_stock_create(self, + stock_name, + belong_merchant, + available_begin_time, + available_end_time, + stock_use_rule, + coupon_use_rule, + out_request_no, + stock_type='NORMAL', + no_cash=False, + comment=None, + pattern_info=None, + ext_info=None): + """创建代金券批次 + :param stock_name: 批次名称,示例值:'微信支付代金券批次' + :param belong_merchant: 归属商户号。示例值:'98568865' + :param available_begin_time: 可用时间-开始时间,格式为YYYY-MM-DDTHH:mm:ss.sss+TIMEZONE。示例值:'2015-05-20T13:29:35.120+08:00' + :param available_end_time: 可用时间-结束时间,格式为YYYY-MM-DDTHH:mm:ss.sss+TIMEZONE。示例值:'2015-05-20T13:29:35.120+08:00' + :param stock_use_rule: 发放规则。示例值:{'max_coupons':5, 'max_amount':100, 'max_coupons_per_user':1, 'natural_person_limit':False, 'prevent_api_abuse':True} + :param coupon_use_rule: 核销规则。示例值:{'available_merchants':['9856000','9856111']} + :param out_request_no: 商户单据号,可包含英文字母,数字,|,_,*,-等内容,不允许出现其他不合法符号,商户侧需保持商户单据号全局唯一。示例值:'89560002019101000121' + :param stock_type: 批次类型,仅支持:'NORMAL':固定面额满减券批次。示例值:'NORMAL' + :param no_cash: 营销经费,枚举值:True:免充值,False:预充值。示例值:False + :param comment: 批次备注,仅制券商户可见,用于自定义信息。校验规则:批次备注最多60个UTF8字符数。示例值:'零售批次' + :param pattern_info: 样式设置,示例值:{'description':'微信支付营销代金券'} + :param ext_info: 扩展属性,json格式字符串,如无需要则不填写。示例值:"{'exinfo1':'1234','exinfo2':'3456'}" + """ + params = {} + if stock_name: + params.update({'stock_name': stock_name}) + else: + raise Exception('stock_name is not assigned.') + if belong_merchant: + params.update({'belong_merchant': belong_merchant}) + else: + raise Exception('belong_merchant is not assigned.') + if available_begin_time: + params.update({'available_begin_time': available_begin_time}) + else: + raise Exception('available_begin_time is not assigned.') + if available_end_time: + params.update({'available_end_time': available_end_time}) + else: + raise Exception('available_end_time is not assigned.') + if stock_use_rule: + params.update({'stock_use_rule': stock_use_rule}) + else: + raise Exception('stock_use_rule is not assigned.') + if coupon_use_rule: + params.update({'coupon_use_rule': coupon_use_rule}) + else: + raise Exception('coupon_use_rule is not assigned.') + if stock_type: + params.update({'stock_type': stock_type}) + else: + raise Exception('stock_type is not assigned.') + if out_request_no: + params.update({'out_request_no': out_request_no}) + else: + raise Exception('out_request_no is not assigned.') + if no_cash: + params.update({'no_cash': no_cash}) + if comment: + params.update({'comment': comment}) + if pattern_info: + params.update({'pattern_info': pattern_info}) + if ext_info: + params.update({'ext_info': ext_info}) + path = '/v3/marketing/favor/coupon-stocks' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def marketing_favor_stock_start(self, stock_creator_mchid, stock_id): + """激活代金券批次 + :param stock_creator_mchid: 创建批次的商户号,示例值:'8956000' + :param stock_id: 批次号,示例值:'9856000' + """ + params = {} + if stock_creator_mchid: + params.update({'stock_creator_mchid': stock_creator_mchid}) + else: + raise Exception('stock_creator_mchid is not assigned.') + if stock_id: + path = '/v3/marketing/favor/stocks/%s/start' % stock_id + else: + raise Exception('stock_id is not assigned.') + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def marketing_favor_stock_send(self, + stock_id, + openid, + out_request_no, + stock_creator_mchid, + coupon_value=None, + coupon_minimum=None, + appid=None): + """发放代金券批次 + :param stock_id: 批次号。示例值:'9856000' + :param openid: 用户openid,示例值:'2323dfsdf342342' + :param out_request_no: 商户单据号,示例值: '89560002019101000121' + :param stock_creator_mchid: 创建批次的商户号,示例值:'8956000' + :param coupon_value: 指定面额发券,面额。示例值:100 + :param coupon_minimum: 指定面额发券,券门槛。示例值:100 + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if stock_id: + params.update({'stock_id': stock_id}) + else: + raise Exception('stock_id is not assigned.') + if openid: + path = '/v3/marketing/favor/users/%s/coupons' % openid + else: + raise Exception('openid is not assigned.') + if out_request_no: + params.update({'out_request_no': out_request_no}) + else: + raise Exception('out_request_no is not assigned.') + if stock_creator_mchid: + params.update({'stock_creator_mchid': stock_creator_mchid}) + else: + raise Exception('stock_creator_mchid is not assigned.') + if coupon_value: + params.update({'coupon_value': coupon_value}) + if coupon_minimum: + params.update({'coupon_minimum': coupon_minimum}) + params.update({'appid': appid or self._appid}) + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def marketing_favor_stock_pause(self, stock_creator_mchid, stock_id): + """暂停代金券批次 + :param stock_creator_mchid: 创建批次的商户号,示例值:'8956000' + :param stock_id: 批次号,示例值:'9856000' + """ + params = {} + if stock_creator_mchid: + params.update({'stock_creator_mchid': stock_creator_mchid}) + else: + raise Exception('stock_creator_mchid is not assigned.') + if stock_id: + path = '/v3/marketing/favor/stocks/%s/pause' % stock_id + else: + raise Exception('stock_id is not assigned.') + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def marketing_favor_stock_restart(self, stock_creator_mchid, stock_id): + """重启代金券批次 + :param stock_creator_mchid: 创建批次的商户号,示例值:'8956000' + :param stock_id: 批次号,示例值:'9856000' + """ + params = {} + if stock_creator_mchid: + params.update({'stock_creator_mchid': stock_creator_mchid}) + else: + raise Exception('stock_creator_mchid is not assigned.') + if stock_id: + path = '/v3/marketing/favor/stocks/%s/restart' % stock_id + else: + raise Exception('stock_id is not assigned.') + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def marketing_favor_stock_list(self, + stock_creator_mchid, + offset=0, + limit=10, + create_start_time=None, + create_end_time=None, + status=None): + """条件查询批次列表 + :param stock_creator_mchid: 创建批次的商户号,示例值:'8956000' + :param offset: 分页页码,页码从0开始,默认第0页。示例值:0 + :param limit: 分页大小,最大10。示例值:8 + :param create_start_time: 起始创建时间,格式为YYYY-MM-DDTHH:mm:ss.sss+TIMEZONE。示例值:'2015-05-20T13:29:35.120+08:00' + :param create_end_time: 终止创建时间,格式为YYYY-MM-DDTHH:mm:ss.sss+TIMEZONE。示例值:'2015-05-20T13:29:35.120+08:00' + :param status: 批次状态,枚举值:'unactivated':未激活,'audit':审核中,'running':运行中,'stoped':已停止,'paused':暂停发放。示例值:'paused' + """ + if stock_creator_mchid: + path = '/v3/marketing/favor/stocks?offset=%s&limit=%s&stock_creator_mchid=%s' % (offset, limit, stock_creator_mchid) + else: + raise Exception('stock_creator_mchid is not assigned.') + if create_start_time: + path = '%s&create_start_time=%s' % (path, create_start_time) + if create_end_time: + path = '%s&create_end_time=%s' % (path, create_end_time) + if status: + path = '%s&status=%s' % (path, status) + return await self._core.request(path) + + +async def marketing_favor_stock_detail(self, stock_creator_mchid, stock_id): + """查询批次详情 + :param stock_creator_mchid: 创建批次的商户号,示例值:'8956000' + :param stock_id: 批次号,示例值:'9856000' + """ + if stock_id: + path = '/v3/marketing/favor/stocks/%s' % stock_id + else: + raise Exception('stock_id is not assigned.') + if stock_creator_mchid: + path = '%s?stock_creator_mchid=%s' % (path, stock_creator_mchid) + else: + raise Exception('stock_creator_mchid is not assigned.') + return await self._core.request(path) + + +async def marketing_favor_coupon_detail(self, coupon_id, openid): + """查询代金券详情 + :param coupon_id: 代金券id,示例值:'9856888' + :param openid: 用户openid,示例值:'2323dfsdf342342' + """ + if coupon_id and openid: + path = '/v3/marketing/favor/users/%s/coupons/%s?appid=%s' % (openid, coupon_id, self._appid) + else: + raise Exception('coupon_id or openid is not assigned.') + return await self._core.request(path) + + +async def marketing_favor_stock_merchant(self, stock_creator_mchid, stock_id, offset=0, limit=50): + """查询代金券可用商户 + :param stock_creator_mchid: 创建批次的商户号,示例值:'8956000' + :param stock_id: 批次号,示例值:'9856000' + :param offset: 分页页码,最大1000。示例值: 10 + :param limit: 分页大小,最大50。示例值: 10 + """ + if stock_id: + path = '/v3/marketing/favor/stocks/%s/merchants' % stock_id + else: + raise Exception('stock_id is not assigned.') + if stock_creator_mchid: + path = '%s?stock_creator_mchid=%s&offset=%s&limit=%s&' % (path, stock_creator_mchid, offset, limit) + return await self._core.request(path) + + +async def marketing_favor_stock_item(self, stock_creator_mchid, stock_id, offset=0, limit=50): + """查询代金券可用单品 + :param stock_creator_mchid: 创建批次的商户号,示例值:'8956000' + :param stock_id: 批次号,示例值:'9856000' + :param offset: 分页页码,最大500。示例值: 10 + :param limit: 分页大小,最大100。示例值: 10 + """ + if stock_id: + path = '/v3/marketing/favor/stocks/%s/items' % stock_id + else: + raise Exception('stock_id is not assigned.') + if stock_creator_mchid: + path = '%s?stock_creator_mchid=%s&offset=%s&limit=%s&' % (path, stock_creator_mchid, offset, limit) + return await self._core.request(path) + + +async def marketing_favor_user_coupon(self, + openid, + stock_id=None, + status=None, + creator_mchid=None, + sender_mchid=None, + available_mchid=None, + offset=0, + limit=20): + """根据商户号查用户的券 + :param openid: 用户openid,示例值:'2323dfsdf342342' + :param stock_id: 批次号,示例值:'9856000' + :param status: 券状态,代金券状态:'SENDED':可用,'USED':已实扣,填写available_mchid参数则该字段不生效。示例值:'USED' + :param creator_mchid: 创建批次的商户号.示例值:'9865002' + :param sender_mchid: 批次发放商户号。示例值:'9865001' + :param available_mchid: 可用商户号。示例值: '9865000' + :param offset: 分页页码,默认0,填写available_mchid,该字段不生效。示例值:0 + :param limit: 分页大小,默认20,填写available_mchid,该字段不生效。示例值:20 + """ + if openid: + path = '/v3/marketing/favor/users/%s/coupons?appid=%s&offset=%s&limit=%s' % (openid, self._appid, offset, limit) + else: + raise Exception('openid is not assigned.') + if stock_id: + path = '%s&stock_id=%s' % (path, stock_id) + if status: + path = '%s&status=%s' % (path, status) + if creator_mchid: + path = '%s&creator_mchid=%s' % (path, creator_mchid) + elif sender_mchid: + path = '%s&sender_mchid=%s' % (path, sender_mchid) + elif available_mchid: + path = '%s&available_mchid=%s' % (path, available_mchid) + return await self._core.request(path) + + +async def marketing_favor_use_flow(self, stock_id): + """下载批次核销明细 + :param stock_id: 批次号,微信为每个代金券批次分配的唯一id。示例值:'9865000' + """ + if stock_id: + path = '/v3/marketing/favor/stocks/%s/use-flow' % stock_id + else: + raise Exception('stock_id is not assigned.') + return await self._core.request(path) + + +async def marketing_favor_refund_flow(self, stock_id): + """下载批次退款明细 + :param stock_id: 批次号,微信为每个代金券批次分配的唯一id。示例值:'9865000' + """ + if stock_id: + path = '/v3/marketing/favor/stocks/%s/refund-flow' % stock_id + else: + raise Exception('stock_id is not assigned.') + return await self._core.request(path) + + +async def marketing_favor_callback_update(self, notify_url=None, switch=True, mchid=None): + """设置消息通知地址 + :param notify_url: 支付通知商户url地址。示例值:'https://pay.weixin.qq.com' + :param switch: 回调开关,枚举值:True:开启推送,False:停止推送。示例值:True + :param mchid: 微信支付商户号,可不填,默认传入初始化的mchid。示例值:'9856888' + """ + params = {} + params.update({'mchid': mchid or self._mchid}) + if not (notify_url or self._notify_url): + raise Exception('notify_url is not assigned.') + params.update({'notify_url': notify_url or self._notify_url}) + params.update({'switch': switch}) + path = '/v3/marketing/favor/callbacks' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def marketing_busifavor_stock_create(self, + stock_name, + belong_merchant, + goods_name, + stock_type, + coupon_use_rule, + stock_send_rule, + out_request_no, + coupon_code_mode, + comment=None, + custom_entrance=None, + display_pattern_info=None, + notify_config=None, + subsidy=False): + """创建商家券 + :params stock_name: 商家券批次名称,字数上限为21个,一个中文汉字/英文字母/数字均占用一个字数。示例值:'8月1日活动券' + :params belong_merchant: 批次归属商户号。注:普通直连模式,该参数为直连商户号。示例值:'10000022' + :params goods_name: 适用商品范围,用来描述批次在哪些商品可用,会显示在微信卡包中。字数上限为15个。示例值:'xxx商品使用' + :params stock_type: 批次类型,'NORMAL':固定面额满减券批次,'DISCOUNT':折扣券批次,'EXCHANGE':换购券批次。示例值:'NORMAL' + :params coupon_use_rule: 核销规则。示例值:{'coupon_available_time':{}, 'fixed_normal_coupon':{}, 'use_method':'OFF_LINE', } + :params stock_send_rule: 发放规则。示例值:{'max_coupons':100, 'max_coupons_per_user':5} + :params out_request_no: 商户请求单号。示例值:'100002322019090134234sfdf' + :params coupon_code_mode: 券code模式,枚举值:'WECHATPAY_MODE':系统分配券code。(固定22位纯数字),'MERCHANT_API':商户发放时接口指定券code,'MERCHANT_UPLOAD':商户上传自定义code,发券时系统随机选取上传的券code。示例值:'WECHATPAY_MODE' + :params comment: 批次备注,仅配置商户可见,用于自定义信息。字数上限为20个。示例值:'活动使用' + :params custom_entrance: 自定义入口。示例值:{'hall_id':'233455656'} + :params display_pattern_info: 样式信息。示例值:{'description':'xxx门店可用'} + :params notify_config: 事件通知配置。示例值:{'notify_appid':'wx23232232323'} + :params subsidy=False: 是否允许营销补贴,该批次发放的券是否允许进行补差。示例值:False + """ + params = {} + if stock_name: + params.update({'stock_name': stock_name}) + else: + raise Exception('stock_name is not assigned.') + if belong_merchant: + params.update({'belong_merchant': belong_merchant}) + else: + raise Exception('belong_merchant is not assigned.') + if goods_name: + params.update({'goods_name': goods_name}) + else: + raise Exception('goods_name is not assigned.') + if stock_type: + params.update({'stock_type': stock_type}) + else: + raise Exception('stock_type is not assigned.') + if coupon_use_rule: + params.update({'coupon_use_rule': coupon_use_rule}) + else: + raise Exception('coupon_use_rule is not assigned.') + if stock_send_rule: + params.update({'stock_send_rule': stock_send_rule}) + else: + raise Exception('stock_send_rule is not assigned.') + if out_request_no: + params.update({'out_request_no': out_request_no}) + else: + raise Exception('out_request_no is not assigned.') + if coupon_code_mode: + params.update({'coupon_code_mode': coupon_code_mode}) + else: + raise Exception('coupon_code_mode is not assigned.') + if comment: + params.update({'comment': comment}) + if custom_entrance: + params.update({'custom_entrance': custom_entrance}) + if display_pattern_info: + params.update({'display_pattern_info': display_pattern_info}) + if notify_config: + params.update({'notify_config': notify_config}) + params.update({'subsidy': subsidy}) + path = '/v3/marketing/busifavor/stocks' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def marketing_busifavor_stock_query(self, stock_id): + """查询商家券详情 + :param stock_id: 批次号。示例值:1212 + """ + if stock_id: + path = '/v3/marketing/busifavor/stocks/%s' % stock_id + else: + raise Exception('stock_id is not assigned.') + return await self._core.request(path) + + +async def marketing_busifavor_coupon_use(self, + coupon_code, + use_time, + use_request_no, + stock_id=None, + openid=None, + appid=None): + """核销用户券 + :param coupon_code: 券code,券的唯一标识。示例值:'sxxe34343434' + :param use_time: 请求核销时间,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE。示例值:'2015-05-20T13:29:35+08:00' + :param use_request_no: 核销请求单据号,每次核销请求的唯一标识,商户需保证唯一。示例值:'1002600620019090123143254435' + :param stock_id: 批次号。示例值:1212 + :param openid: 用户标识。示例值:'xsd3434454567676' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if coupon_code: + params.update({'coupon_code': coupon_code}) + else: + raise Exception('coupon_code is not assigned.') + if use_time: + params.update({'use_time': use_time}) + else: + raise Exception('use_time is not assigned.') + if use_request_no: + params.update({'use_request_no': use_request_no}) + else: + raise Exception('use_request_no is not assigned.') + if stock_id: + params.update({'stock_id': stock_id}) + if openid: + params.update({'openid': openid}) + params.update({'appid': appid or self._appid}) + path = '/v3/marketing/busifavor/coupons/use' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def marketing_busifavor_user_coupon(self, + openid, + stock_id=None, + coupon_state=None, + creator_merchant=None, + belong_merchant=None, + sender_merchant=None, + offset=0, + limit=20): + """根据过滤条件查询用户券 + :param openid: 用户标识。示例值:'xsd3434454567676' + :param stock_id: 批次号。示例值:1212 + :param coupon_state: 券状态,枚举值:'SENDED':可用,'USED':已核销,'EXPIRED':已过期,示例值:'SENDED' + :param creator_merchant: 创建批次的商户号。示例值:'1000000001' + :param belong_merchant: 批次归属商户号。示例值:'1000000002' + :param sender_merchant: 批次发放商户号。示例值:'1000000003' + :param offset: 分页页码。示例值:0 + :param limit: 分页大小。示例值:20 + """ + if openid: + path = '/v3/marketing/busifavor/users/%s/coupons?appid=%s&offset=%s&limit=%s' % (openid, self._appid, offset, limit) + else: + raise Exception('openid is not assigned.') + if stock_id: + path = '%s&stock_id=%s' % (path, stock_id) + if coupon_state: + path = '%s&coupon_state=%s' % (path, coupon_state) + if creator_merchant: + path = '%s&creator_merchant=%s' % (path, creator_merchant) + if belong_merchant: + path = '%s&belong_merchant=%s' % (path, belong_merchant) + if sender_merchant: + path = '%s&sender_merchant=%s' % (path, sender_merchant) + return await self._core.request(path) + + +async def marketing_busifavor_coupon_detail(self, coupon_code, openid): + """查询用户单张券详情 + :param coupon_code: 券code,券的唯一标识。示例值:'sxxe34343434' + :param openid: 用户标识。示例值:'xsd3434454567676' + """ + if not (coupon_code and openid): + raise Exception('coupon_code or openid is not assigned.') + path = '/v3/marketing/busifavor/users/%s/coupons/%s/appids/%s' % (openid, coupon_code, self._appid) + return await self._core.request(path) + + +async def marketing_busifavor_couponcode_upload(self, + stock_id, + upload_request_no, + coupon_code_list=[]): + """上传预存code + :param stock_id: 批次号。示例值:1212 + :param upload_request_no: 请求业务单据号。商户上传code的凭据号,商户侧需保持唯一性。示例值:'100002322019090134234sfdf' + :param coupon_code_list: 券code列表。示例值:['ABC9588200','ABC9588201'] + """ + params = {} + if stock_id: + path = '/v3/marketing/busifavor/stocks/%s/couponcodes' % stock_id + else: + raise Exception('stock_id is not assigned.') + if upload_request_no: + params.update({'upload_request_no': upload_request_no}) + else: + raise Exception('upload_request_no is not assigned.') + if coupon_code_list: + params.update({'coupon_code_list': coupon_code_list}) + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def marketing_busifavor_callback_update(self, mchid=None, notify_url=None): + """设置商家券事件通知地址 + :param mchid: 商户号,可不填,默认传入初始化的mchid。示例值:'10000098' + :param notify_url: 通知URL地址,用于接收商家券事件通知的url地址,不填默认使用初始化的notify_url。示例值:'https://pay.weixin.qq.com' + """ + params = {} + params.update({'mchid': mchid or self._mchid}) + if not (notify_url or self._notify_url): + raise Exception('notify_url is not assigned.') + params.update({'notify_url': notify_url or self._notify_url}) + path = '/v3/marketing/busifavor/callbacks' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def marketing_busifavor_callback_query(self, mchid=None): + """查询商家券事件通知地址 + :param mchid: 商户号,不填默认使用初始化的mchid。示例值:'10000098' + """ + path = '/v3/marketing/busifavor/callbacks?mchid=%s' % (mchid or self._mchid) + return await self._core.request(path) + + +async def marketing_busifavor_coupon_associate(self, stock_id, coupon_code, out_trade_no, out_request_no): + """关联订单信息 + :param stock_id: 批次号。示例值:1212 + :param coupon_code: 券code,券的唯一标识。示例值:'sxxe34343434' + :param out_trade_no: 关联的商户订单号,微信支付下单时的商户订单号,欲与该商家券关联的微信支付。示例值:'MCH_102233445' + :param out_request_no: 商户请求单号,示例值:'1002600620019090123143254435' + """ + params = {} + if stock_id: + params.update({'stock_id': stock_id}) + else: + raise Exception('stock_id is not assigned.') + if coupon_code: + params.update({'coupon_code': coupon_code}) + else: + raise Exception('coupon_code is not assigned.') + if out_trade_no: + params.update({'out_trade_no': out_trade_no}) + else: + raise Exception('out_trade_no is not assigned.') + if out_request_no: + params.update({'out_request_no': out_request_no}) + else: + raise Exception('out_request_no is not assigned.') + path = '/v3/marketing/busifavor/coupons/associate' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def marketing_busifavor_coupon_disassociate(self, stock_id, coupon_code, out_trade_no, out_request_no): + """取消关联订单信息 + :param stock_id: 批次号。示例值:1212 + :param coupon_code: 券code,券的唯一标识。示例值:'sxxe34343434' + :param out_trade_no: 关联的商户订单号,微信支付下单时的商户订单号,欲与该商家券关联的微信支付。示例值:'MCH_102233445' + :param out_request_no: 商户请求单号,示例值:'1002600620019090123143254435' + """ + params = {} + if stock_id: + params.update({'stock_id': stock_id}) + else: + raise Exception('stock_id is not assigned.') + if coupon_code: + params.update({'coupon_code': coupon_code}) + else: + raise Exception('coupon_code is not assigned.') + if out_trade_no: + params.update({'out_trade_no': out_trade_no}) + else: + raise Exception('out_trade_no is not assigned.') + if out_request_no: + params.update({'out_request_no': out_request_no}) + else: + raise Exception('out_request_no is not assigned.') + path = '/v3/marketing/busifavor/coupons/disassociate' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def marketing_busifavor_stock_budget(self, + stock_id, + modify_budget_request_no, + target_max_coupons=None, + target_max_coupons_by_day=None, + current_max_coupons=None, + current_max_coupons_by_day=None): + """修改批次预算 + :param stock_id: 批次号。示例值:1212 + :param modify_budget_request_no: 修改预算请求单据号,示例值:'1002600620019090123143254436' + :param target_max_coupons: 目标批次最大发放个数。示例值:3000 + :param target_max_coupons_by_day: 目标单天发放上限个数。示例值:500 + :param current_max_coupons: 当前批次最大发放个数。示例值:500 + :param current_max_coupons_by_day: 当前单天发放上限个数。示例值:300 + """ + params = {} + if stock_id: + path = '/v3/marketing/busifavor/stocks/%s/budget' % stock_id + else: + raise Exception('stock_id is not assigned.') + if modify_budget_request_no: + params.update({'modify_budget_request_no': modify_budget_request_no}) + else: + raise Exception('modify_budget_request_no is not assigned.') + if target_max_coupons: + params.update({'target_max_coupons': target_max_coupons}) + elif target_max_coupons_by_day: + params.update({'target_max_coupons_by_day': target_max_coupons_by_day}) + else: + raise Exception('target_max_coupons or target_max_coupons_by_day is not assigned.') + if current_max_coupons: + params.update({'current_max_coupons': current_max_coupons}) + if current_max_coupons_by_day: + params.update({'current_max_coupons_by_day': current_max_coupons_by_day}) + return await self._core.request(path, method=RequestType.PATCH, data=params) + + +async def marketing_busifavor_stock_modify(self, + stock_id, + out_request_no, + custom_entrance=None, + comment=None, + goods_name=None, + display_pattern_info=None, + coupon_use_rule=None, + stock_send_rule=None, + notify_config=None): + """修改商家券基本信息 + :param stock_id: 批次号。示例值:1212 + :param out_request_no: 商户请求单号,示例值:'1002600620019090123143254435' + :param custom_entrance: 自定义入口。示例值:{'hall_id':'234567'} + :param comment: 批次备注,字数上限为20个。示例值:'活动使用' + :param goods_name: 适用商品范围。示例值:'xxx商品使用' + :param display_pattern_info: 样式信息。示例值:{'description':'xxx门店可用'} + :param coupon_use_rule: 核销规则。示例值:{'use_method':'OFF_LINE'} + :param stock_send_rule: 发放规则。示例值:{'prevent_api_abuse':False} + :param notify_config: 事件通知配置。示例值:{'notify_appid':'wx23232232323'} + """ + if stock_id: + path = '/v3/marketing/busifavor/stocks/%s' % stock_id + else: + raise Exception('stock_id is not assigned.') + params = {} + if out_request_no: + params.update({'out_request_no': out_request_no}) + else: + raise Exception('out_request_no is not assigned.') + if custom_entrance: + params.update({'custom_entrance': custom_entrance}) + if comment: + params.update({'comment': comment}) + if goods_name: + params.update({'goods_name': goods_name}) + if display_pattern_info: + params.update({'display_pattern_info': display_pattern_info}) + if coupon_use_rule: + params.update({'coupon_use_rule': coupon_use_rule}) + if stock_send_rule: + params.update({'stock_send_rule': stock_send_rule}) + if notify_config: + params.update({'notify_config': notify_config}) + return await self._core.request(path, method=RequestType.PATCH, data=params) + + +async def marketing_busifavor_coupon_return(self, coupon_code, stock_id, return_request_no): + """申请退券 + :param coupon_code: 券code,券的唯一标识。示例值:'sxxe34343434' + :param stock_id: 批次号。示例值:1212 + :param return_request_no: 退券请求单据号。示例值:'1002600620019090123143254436' + """ + params = {} + if coupon_code: + params.update({'coupon_code': coupon_code}) + else: + raise Exception('coupon_code is not assigned.') + if stock_id: + params.update({'stock_id': stock_id}) + else: + raise Exception('stock_id is not assigned.') + if return_request_no: + params.update({'return_request_no': return_request_no}) + else: + raise Exception('return_request_no is not assigned.') + path = '/v3/marketing/busifavor/coupons/return' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def marketing_busifavor_coupon_deactivate(self, coupon_code, stock_id, deactivate_request_no, deactivate_reason=None): + """使券失效 + :param coupon_code: 券code,券的唯一标识。示例值:'sxxe34343434' + :param stock_id: 批次号。示例值:1212 + :param deactivate_request_no: 失效请求单据号。示例值:'1002600620019090123143254436' + :param deactivate_reason: 失效原因。示例值:'此券使用时间设置错误' + """ + params = {} + if coupon_code: + params.update({'coupon_code': coupon_code}) + else: + raise Exception('coupon_code is not assigned.') + if stock_id: + params.update({'stock_id': stock_id}) + else: + raise Exception('stock_id is not assigned.') + if deactivate_request_no: + params.update({'deactivate_request_no': deactivate_request_no}) + else: + raise Exception('deactivate_request_no is not assigned.') + if deactivate_reason: + params.update({'deactivate_reason': deactivate_reason}) + path = '/v3/marketing/busifavor/coupons/deactivate' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def marketing_busifavor_subsidy_pay(self, + stock_id, + coupon_code, + transaction_id, + payer_merchant, + payee_merchant, + amount, + description, + out_subsidy_no): + """营销补差付款 + :param stock_id: 批次号。示例值:1212 + :param coupon_code: 券code,券的唯一标识。示例值:'sxxe34343434' + :param transaction_id: 微信支付订单号。示例值:'4200000913202101152566792388' + :param payer_merchant: 营销补差扣款商户号。示例值:'1900000001' + :param payee_merchant: 营销补差入账商户号。示例值:'1900000002' + :param amount: 补差付款金额。示例值:100 + :param description: 补差付款描述。示例值:'20210115DESCRIPTION' + :param out_subsidy_no: 业务请求唯一单号。示例值:'subsidy-abcd-12345678' + """ + params = {} + if coupon_code: + params.update({'coupon_code': coupon_code}) + else: + raise Exception('coupon_code is not assigned.') + if stock_id: + params.update({'stock_id': stock_id}) + else: + raise Exception('stock_id is not assigned.') + if transaction_id: + params.update({'transaction_id': transaction_id}) + else: + raise Exception('transaction_id is not assigned.') + if payer_merchant: + params.update({'payer_merchant': payer_merchant}) + else: + raise Exception('payer_merchant is not assigned.') + if payee_merchant: + params.update({'payee_merchant': payee_merchant}) + else: + raise Exception('payee_merchant is not assigned.') + if amount: + params.update({'amount': amount}) + else: + raise Exception('amount is not assigned.') + if description: + params.update({'description': description}) + else: + raise Exception('description is not assigned.') + if out_subsidy_no: + params.update({'out_subsidy_no': out_subsidy_no}) + else: + raise Exception('out_subsidy_no is not assigned.') + path = '/v3/marketing/busifavor/subsidy/pay-receipts' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def marketing_busifavor_subsidy_query(self, subsidy_receipt_id): + """查询营销补差付款单详情 + :param subsidy_receipt_id: 补差付款单号。示例值:'1120200119165100000000000001' + """ + if subsidy_receipt_id: + path = '/v3/marketing/busifavor/subsidy/pay-receipts/%s' % subsidy_receipt_id + else: + raise Exception('subsidy_receipt_id is not assigned.') + return await self._core.request(path) + + +async def industry_coupon_token(self, open_id, coupon_list=[]): + """出行券切卡组件预下单 + https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter9_9_1.shtml + :param open_id: 用户在商户AppID下的唯一标识,该用户为后续拉起切卡组件的用户。示例值:'obLatjrR8kUDlj4-nofQsPAJAAFI' + :param coupon_list: 用户最近领取的出行券列表。示例值:[{"coupon_id": "11004999626", "stock_id": 16474341}] + """ + params = {} + if open_id: + params.update({'open_id': open_id}) + else: + raise Exception('open_id is not assigned.') + if coupon_list: + params.update({'coupon_list': coupon_list}) + else: + raise Exception('coupon_list is not assigned.') + path = '/v3/industry-coupon/tokens' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def bank_package_file(self, package_id, bank_type, filepath): + """导入定向用户协议号 + https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter9_8_1.shtml + :package_id: 号码包唯一标识符。可在微信支付商户平台创建号码包后获得。示例值:'8473295' + :filepath: 电子发票文件路径,只支持txt和csv两种格式,示例值:'./active_user.csv' + """ + if not (filepath and os.path.exists(filepath) and os.path.isfile(filepath)): + raise Exception('filepath is not assigned or not exists') + with open(filepath, mode='rb') as f: + content = f.read() + filename = os.path.basename(filepath) + filetype = os.path.splitext(filename)[-1][1:].upper() + mimes = { + 'TXT': ' text/plain', + 'CSV': 'text/csv' + } + if filetype not in mimes: + raise Exception(f'wechatpayv3 does not support this file type: {filetype}') + if not package_id or bank_type: + raise Exception('package_id or bank_type is not assigned.') + params = {} + params.update({'meta': '{"bank_type":"%s", "filename":"%s", "sha256":"%s"}' % (bank_type, filename, sha256(content))}) + files = [('file', (filename, content, mimes[filetype]))] + path = '/v3/marketing/bank/packages/%s/tasks' % package_id + return await self._core.request(path, method=RequestType.POST, data=params, sign_data=params.get('meta'), files=files) diff --git a/wechatpayv3/async_/mchtransfer.py b/wechatpayv3/async_/mchtransfer.py new file mode 100644 index 0000000..5ec9fd1 --- /dev/null +++ b/wechatpayv3/async_/mchtransfer.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType + +async def mch_transfer_bills(self, out_bill_no, transfer_scene_id, openid, transfer_amount, transfer_remark, user_name=None, user_recv_perception=None, transfer_scene_report_infos=[], appid=None, notify_url=None): + """发起转账 + :param out_bill_no: 商户单号,商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一,示例值:'plfk2020042013' + :param transfer_scene_id: 转账场景ID,示例值:'1001' + :param openid: 收款用户OpenID,商户AppID下,某用户的OpenID,示例值:'o-MYE42l80oelYMDE34nYD456Xoy' + :param transfer_amount: 转账金额,单位为“分”,示例值: 1000 + :param transfer_remark: 转账备注,用户收款时可见该备注信息,最多允许32个字符,示例值:'2020年4月报销' + :param user_name: 收款用户姓名,转账金额 >= 2,000元时,该笔明细必须填写。若商户传入收款用户姓名,微信支付会校验收款用户与输入姓名是否一致,并提供电子回单,示例值:'张三' + :param user_recv_perception: 用户收款时感知到的收款原因,将根据转账场景自动展示默认内容。如有其他展示需求,可在本字段传入。示例值: '现金奖励' + :param transfer_scene_report_infos: 转账场景报备信息,info_type的值必需按文档指示传入,示例值: [{'info_type':'活动名称', 'info_content':'新会员有礼'}, {'info_type':'奖励说明', 'info_content':'注册会员抽奖'}] + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param notify_url: 通知地址,异步接收微信支付结果通知的回调地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php' + """ + params={} + if out_bill_no: + params.update({'out_bill_no':out_bill_no}) + else: + raise Exception('out_batch_no is not assigned') + if transfer_scene_id: + params.update({'transfer_scene_id':transfer_scene_id}) + else: + raise Exception('transfer_scene_id is not assigned') + if openid: + params.update({'openid':openid}) + else: + raise Exception('openid is not assigned') + if transfer_amount: + params.update({'transfer_amount':transfer_amount}) + else: + raise Exception('transfer_amount is not assigned') + if transfer_remark: + params.update({'transfer_remark':transfer_remark}) + else: + raise Exception('transfer_remark is not assigned') + cipher_data = False + if user_name and transfer_amount >= 30: + params.update({'user_name':self._core.encrypt(user_name)}) + cipher_data = True + if transfer_amount >= 200000 and not user_name: + raise Exception('user_name is not assigned') + if user_recv_perception: + params.update({'user_recv_perception':user_recv_perception}) + if transfer_scene_report_infos: + params.update({'transfer_scene_report_infos':transfer_scene_report_infos}) + params.update({'appid': appid or self._appid}) + params.update({'notify_url': notify_url or self._notify_url}) + path = '/v3/fund-app/mch-transfer/transfer-bills' + return await self._core.request(path, method=RequestType.POST, data=params, cipher_data=cipher_data) + +async def mch_transfer_bills_cancel(self, out_bill_no): + """撤销转账 + :param out_bill_no: 商户单号,商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一,示例值:'plfk2020042013' + """ + if out_bill_no: + path = f'/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}/cancel' + else: + raise Exception('out_bill_no is not assigned') + return await self._core.request(path, method=RequestType.POST) + +async def mch_transfer_bills_query(self, out_bill_no=None, transfer_bill_no=None): + """查询转账单 + :param out_bill_no: 商户单号,商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一,示例值:'plfk2020042013' + :param transfer_bill_no: 微信转账单号,微信商家转账系统返回的唯一标识,示例值: '1330000071100999991182020050700019480001' + """ + if not (out_bill_no or transfer_bill_no): + raise Exception('out_bill_no or transfer_bill_no is not assigned') + if out_bill_no: + path = f'/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}' + else: + path = f'/v3/fund-app/mch-transfer/transfer-bills/transfer-bill-no/{transfer_bill_no}' + return await self._core.request(path) + +async def mch_transfer_elecsign(self, out_bill_no=None, transfer_bill_no=None): + """申请电子回单 + :param out_bill_no: 商户单号,商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一,示例值:'plfk2020042013' + :param transfer_bill_no: 微信转账单号,微信商家转账系统返回的唯一标识,示例值: '1330000071100999991182020050700019480001' + """ + if not (out_bill_no or transfer_bill_no): + raise Exception('out_bill_no or transfer_bill_no is not assigned') + params = {} + if out_bill_no: + params.update({'out_bill_no':out_bill_no}) + path = '/v3/fund-app/mch-transfer/elecsign/out-bill-no' + else: + params.update({'transfer_bill_no':transfer_bill_no}) + path = '/v3/fund-app/mch-transfer/elecsign/transfer-bill-no' + return await self._core.request(path, method=RequestType.POST, data=params) + +async def mch_transfer_elecsign_query(self, out_bill_no=None, transfer_bill_no=None): + """查询电子回单 + :param out_bill_no: 商户单号,商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一,示例值:'plfk2020042013' + :param transfer_bill_no: 微信转账单号,微信商家转账系统返回的唯一标识,示例值: '1330000071100999991182020050700019480001' + """ + if not (out_bill_no or transfer_bill_no): + raise Exception('out_bill_no or transfer_bill_no is not assigned') + if out_bill_no: + path = f'/v3/fund-app/mch-transfer/elecsign/out-bill-no/{out_bill_no}' + else: + path = f'/v3/fund-app/mch-transfer/elecsign/transfer-bill-no/{transfer_bill_no}' + return await self._core.request(path) diff --git a/wechatpayv3/async_/media.py b/wechatpayv3/async_/media.py new file mode 100644 index 0000000..1ae9c8f --- /dev/null +++ b/wechatpayv3/async_/media.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +import os.path + +from .type import RequestType +from .utils import sha256 + + +async def _media_upload(self, filepath, filename, path): + if not (filepath and os.path.exists(filepath) and os.path.isfile(filepath) and path): + raise Exception('filepath is not assigned or not exists') + with open(filepath, mode='rb') as f: + content = f.read() + if not filename: + filename = os.path.basename(filepath) + params = {} + params.update({'meta': '{"filename":"%s","sha256":"%s"}' % (filename, sha256(content))}) + mimes = { + '.bmp': 'image/bmp', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.avi': 'video/x-msvideo', + '.wmv': 'video/x-ms-wmv', + '.mpeg': 'video/mpeg', + '.mp4': 'video/mp4', + '.mov': 'video/quicktime', + '.mkv': 'video/x-matroska', + '.flv': 'video/x-flv', + '.f4v': 'video/x-f4v', + '.m4v': 'video/x-m4v', + '.rmvb': 'application/vnd.rn-realmedia-vbr' + } + media_type = os.path.splitext(filename)[-1] + if media_type not in mimes: + raise Exception(f'wechatpayv3 does not support this media type: {media_type}') + files = [('file', (filename, content, mimes[media_type]))] + return await self._core.request(path, method=RequestType.POST, data=params, sign_data=params.get('meta'), files=files) + + +async def image_upload(self, filepath, filename=None): + """图片上传 + :param filepath: 图片文件路径 + :param filename: 文件名称,未指定则从filepath参数中截取 + """ + return _media_upload(self, filepath, filename, path='/v3/merchant/media/upload') + + +async def video_upload(self, filepath, filename=None): + """视频上传 + :param filepath: 视频文件路径 + :param filename: 文件名称,未指定则从filepath参数中截取 + """ + return _media_upload(self, filepath, filename, path='/v3/merchant/media/video_upload') diff --git a/wechatpayv3/async_/merchantrisk.py b/wechatpayv3/async_/merchantrisk.py new file mode 100644 index 0000000..e68f912 --- /dev/null +++ b/wechatpayv3/async_/merchantrisk.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType + + +async def merchantrisk_callback_create(self, notify_url=None): + """创建商户违规通知回调地址 + :param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php' + """ + params = {} + if notify_url: + params.update({'notify_url': notify_url}) + path = '/v3/merchant-risk-manage/violation-notifications' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def merchantrisk_callback_query(self): + """查询商户违规通知回调地址 + """ + path = '/v3/merchant-risk-manage/violation-notifications' + return await self._core.request(path) + + +async def merchantrisk_callback_update(self, notify_url=None): + """修改商户违规通知回调地址 + :param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php' + """ + params = {} + if notify_url: + params.update({'notify_url': notify_url}) + path = '/v3/merchant-risk-manage/violation-notifications' + return await self._core.request(path, method=RequestType.PUT, data=params) + + +async def merchantrisk_callback_delete(self): + """查询商户违规通知回调地址 + """ + path = '/v3/merchant-risk-manage/violation-notifications' + return await self._core.request(path, method=RequestType.DELETE) diff --git a/wechatpayv3/async_/parking.py b/wechatpayv3/async_/parking.py new file mode 100644 index 0000000..b174c76 --- /dev/null +++ b/wechatpayv3/async_/parking.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType + + +async def parking_service_find(self, plate_number, plate_color, openid, sub_mchid=None): + """查询车牌服务开通信息 + :param plate_number: 车牌号,示例值:'粤B888888' + :param plate_color: 车牌颜色,车牌颜色,枚举值:BLUE:蓝色,GREEN:绿色,YELLOW:黄色,BLACK:黑色,WHITE:白色,LIMEGREEN:黄绿色 + :param openid: 用户标识,示例值:'oUpF8uMuAJOM2pxb1Q' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + path = '/v3/vehicle/parking/services/find?appid=%s' % self._appid + if plate_number: + path = '%s&plate_number=%s' % (path, plate_number) + else: + raise Exception('plate_number is not assigned.') + if plate_color: + path = '%s&plate_color=%s' % (path, plate_color) + else: + raise Exception('plate_color is not assigned.') + if openid: + path = '%s&openid=%s' % (path, openid) + else: + raise Exception('openid is not assigned.') + if self._partner_mode: + if sub_mchid: + path = '%s&sub_mchid=%s' % (path, sub_mchid) + else: + raise Exception('sub_mchid is not assigned.') + return await self._core.request(path) + + +async def parking_enter(self, out_parking_no, plate_number, plate_color, start_time, parking_name, free_duration, notify_url=None, sub_mchid=None): + """创建停车入场 + :param out_parking_no: 商户入场id,商户侧入场标识id,在同一个商户号下唯一,示例值:'1231243' + :param plate_number: 车牌号,示例值:'粤B888888' + :param plate_color: 车牌颜色,车牌颜色,枚举值:BLUE:蓝色,GREEN:绿色,YELLOW:黄色,BLACK:黑色,WHITE:白色,LIMEGREEN:黄绿色 + :param notify_url: 回调通知url,接受入场状态变更回调通知的url,只接受https,示例值:https://yoursite.com/wxpay.html + :param start_time: 入场时间,示例值:'2017-08-26T10:43:39+08:00' + :param parking_name: 停车场名称,示例值:'欢乐海岸停车场' + :param free_duration: 免费时长,单位为秒,示例值:3600 + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if out_parking_no: + params.update({'out_parking_no': out_parking_no}) + else: + raise Exception('out_parking_no is not assigned') + if plate_number: + params.update({'plate_number': plate_number}) + else: + raise Exception('plate_number is not assigned') + if plate_color: + params.update({'plate_color': plate_color}) + else: + raise Exception('plate_color is not assigned') + if not (notify_url or self._notify_url): + raise Exception('notify_url is not assigned.') + params.update({'notify_url': notify_url or self._notify_url}) + if start_time: + params.update({'start_time': start_time}) + else: + raise Exception('start_time is not assigned') + if parking_name: + params.update({'parking_name': parking_name}) + else: + raise Exception('parking_name is not assigned') + if free_duration: + params.update({'free_duration': free_duration}) + else: + raise Exception('free_duration is not assigned') + if self._partner_mode: + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + path = '/v3/vehicle/parking/parkings' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def parking_order(self, description, out_trade_no, total, parking_id, plate_number, plate_color, start_time, + end_time, parking_name, charging_duration, device_id, trade_scene='PARKING', profit_sharing='N', + currency='CNY', attach=None, goods_tag=None, notify_url=None, appid=None, sub_appid=None, sub_mchid=None): + """停车扣费受理 + :param description: 服务描述,商户自定义字段,用于交易账单中对扣费服务的描述。示例值:'停车场扣费' + :param out_trade_no: 商户订单号,商户系统内部订单号,只能是数字、大小写字母,且在同一个商户号下唯一,示例值:'20150806125346' + :param notify_url: 回调通知url,只接受https,示例值:'https://yoursite.com/wxpay.html' + :param total: 订单总金额,单位为分,只能为整数,示例值:888 + :param parking_id: 停车入场id,通过入场通知接口获取的入场id,示例值:'5K8264ILTKCH16CQ250' + :param plate_number: 车牌号,仅包括省份+车牌,不包括特殊字符。示例值:'粤B888888' + :param plate_color: 车牌颜色,枚举值:BLUE:蓝色,GREEN:绿色,YELLOW:黄色,BLACK:黑色,WHITE:白色,LIMEGREEN:黄绿色,示例值:BLUE + :param start_time: 入场时间,示例值:'2017-08-26T10:43:39+08:00' + :param end_time: 出场时间,示例值:'2017-08-26T10:43:39+08:00' + :param parking_name: 停车场名称,示例值:'欢乐海岸停车场' + :param charging_duration: 计费时长,单位为秒,示例值:3600 + :param device_id: 停车场设备id,示例值:'12313' + :param trade_scene: 交易场景值,目前支持'PARKING':车场停车场景 + :param profit_sharing: 分账标识,枚举值:'Y':是,需要分账,'N':否,不分账,字母要求大写,不传默认不分账。 + :param currency: 货币类型,目前只支持人民币:'CNY' + :param attach: 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用,示例值:'深圳分店' + :param goods_tag: 订单优惠标记,代金券或立减优惠功能的参数,示例值:WXG + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param sub_appid: (服务商模式)子商户应用ID,示例值:'wxd678efh567hg6999' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + amount = {} + parking_info = {} + params.update({'appid': appid or self._appid}) + if description: + params.update({'description': description}) + else: + raise Exception('description is not assigned') + if out_trade_no: + params.update({'out_trade_no': out_trade_no}) + else: + raise Exception('out_trade_no is not assigned') + params.update({'notify_url': notify_url or self._notify_url}) + if total: + amount.update({'total': total}) + else: + raise Exception('total is not assigned') + if parking_id: + parking_info.update({'parking_id': parking_id}) + else: + raise Exception('parking_id is not assigned') + if plate_number: + parking_info.update({'plate_number': plate_number}) + else: + raise Exception('plate_number is not assigned') + if plate_color: + parking_info.update({'plate_color': plate_color}) + else: + raise Exception('plate_color is not assigned') + if start_time: + parking_info.update({'start_time': start_time}) + else: + raise Exception('start_time is not assigned') + if end_time: + parking_info.update({'end_time': end_time}) + else: + raise Exception('end_time is not assigned') + if parking_name: + parking_info.update({'parking_name': parking_name}) + else: + raise Exception('parking_name is not assigned') + if charging_duration: + parking_info.update({'charging_duration': charging_duration}) + else: + raise Exception('charging_duration is not assigned') + if device_id: + parking_info.update({'device_id': device_id}) + else: + raise Exception('device_id is not assigned') + if trade_scene: + params.update({'trade_scene': trade_scene}) + else: + raise Exception('trade_scene is not assigned') + if profit_sharing: + params.update({'profit_sharing': profit_sharing}) + else: + raise Exception('profit_sharing is not assigned') + if currency: + amount.update({'currency': currency}) + else: + raise Exception('currency is not assigned') + if attach: + params.update({'attach': attach}) + if goods_tag: + params.update({'goods_tag': goods_tag}) + params.update({'amount': amount}) + params.update({'parking_info': parking_info}) + if self._partner_mode: + if sub_appid: + params.update({'sub_appid': sub_appid}) + else: + raise Exception('sub_appid is not assigned.') + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + path = '/v3/vehicle/transactions/parking' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def parking_order_query(self, out_trade_no, sub_mchid=None): + """停车扣费订单查询 + :param out_trade_no: 商户订单号,商户系统内部订单号,只能是数字、大小写字母,且在同一个商户号下唯一,示例值:'20150806125346' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + if out_trade_no: + path = '/v3/vehicle/transactions/out-trade-no/%s' % out_trade_no + else: + raise Exception('out_trade_no is not assigned') + if self._partner_mode: + if sub_mchid: + path = '%s?sub_mchid=%s' % (path, sub_mchid) + else: + raise Exception('sub_mchid is not assigned.') + return await self._core.request(path) diff --git a/wechatpayv3/async_/payscore.py b/wechatpayv3/async_/payscore.py new file mode 100644 index 0000000..89847fe --- /dev/null +++ b/wechatpayv3/async_/payscore.py @@ -0,0 +1,389 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType +from .transaction import query_refund, refund + + +async def payscore_direct_complete(self, out_order_no, openid, service_id, service_introduction, post_payments, + time_range, total_amount, post_discounts=None, location=None, + profit_sharing=False, goods_tag=None, attach=None, notify_url=None, appid=None): + """创单结单合并 + :param out_order_no: 商户服务订单号,商户系统内部服务订单号(不是交易单号),要求此参数只能由数字、大小写字母_-|*组成,且在同一个商户号下唯一。示例值:'1234323JKHDFE1243252' + :param openid: 用户标识,微信用户在商户对应appid下的唯一标识。示例值:'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o' + :param service_id: 服务ID。示例值:'500001' + :param service_introduction: 服务信息,用于介绍本订单所提供的服务 ,当参数长度超过20个字符时,报错处理。示例值:'某某酒店' + :param post_payments: 付费项目列表,最多包含100条付费项目。 + :param time_range: 服务时间范围。 + :param total_amount: 总金额,总金额 =(完结付费项目1…+完结付费项目n)-(完结商户优惠项目1…+完结商户优惠项目n)。示例值:50000 + :param post_discounts: 商户优惠,付费商户优惠列表,最多包含30条商户优惠。 + :param location: 服务位置,如果传入,用户侧则显示此参数。 + :param profit_sharing: 微信支付服务分账标记,默认为false,枚举值:False:不分账,True:分账。示例值:False + :param goods_tag: 订单优惠标记。示例值:'goods_tag1' + :param attach: 商户数据包。商户数据包可存放本订单所需信息,需要先urlencode后传入。当商户数据包总长度超出256字符时,报错处理。示例值:'Easdfowealsdkjfnlaksjdlfkwqoi&wl3l2sald' + :param notify_url: 商户回调地址,商户接收扣款成功回调通知的地址,服务需要收款时此参数必填;服务无需收款时此参数不填。示例值:'https://api.test.com' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if not (out_order_no and openid and service_id and service_introduction and post_payments and time_range and total_amount): + raise Exception('ut_order_no or openid or service_id or service_introduction or post_payments or time_range or total_amount is not assigned.') + params.update({'appid': appid or self._appid}) + params.update({'out_order_no': out_order_no}) + params.update({'openid': openid}) + params.update({'service_id': service_id}) + params.update({'service_introduction': service_introduction}) + params.update({'post_payments': post_payments}) + params.update({'time_range': time_range}) + params.update({'total_amount': total_amount}) + if post_discounts: + params.update({'post_discounts': post_discounts}) + if location: + params.update({'location': location}) + if profit_sharing: + params.update({'profit_sharing': profit_sharing}) + if goods_tag: + params.update({'goods_tag': goods_tag}) + if attach: + params.update({'attach': attach}) + payment = False + for item in post_payments: + if item.get('amount') > 0: + payment = True + break + if payment: + if not (notify_url or self._notify_url): + raise Exception('notify_url is not assigned.') + params.update({'notify_url': notify_url or self._notify_url}) + path = '/payscore/serviceorder/direct-complete' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def payscore_permission(self, service_id, authorization_code, notify_url=None, appid=None): + """商户预授权 + :param service_id: 服务ID。示例值:'500001' + :param authorization_code: 授权协议号,户系统内部授权协议号,要求此参数只能由数字、大小写字母_-*组成,且在同一个商户号下唯一。示例值:'1234323JKHDFE1243252' + :param notify_url: 通知地址,商户接收授权回调通知的地址。示例值:'http://www.qq.com' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if not (service_id and authorization_code): + raise Exception('service_id or authorization_code is not assigned.') + params.update({'appid': appid or self._appid}) + params.update({'service_id': service_id}) + params.update({'authorization_code': authorization_code}) + params.update({'notify_url': notify_url or self._notify_url}) + path = '/v3/payscore/permissions' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def payscore_permission_query(self, service_id, authorization_code=None, openid=None): + """查询用户授权记录(授权协议号或openid) + :param service_id: 服务ID。示例值:'500001' + :param authorization_code: 授权协议号,户系统内部授权协议号,要求此参数只能由数字、大小写字母_-*组成,且在同一个商户号下唯一。示例值:'1234323JKHDFE1243252' + :param openid: 用户标识,微信用户在商户对应appid下的唯一标识。示例值:'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o' + """ + if not service_id: + raise Exception('service_id is not assigned.') + if authorization_code: + path = '/v3/payscore/permissions/authorization-code/%s?service_id=%s' % (authorization_code, service_id) + elif openid: + path = '/v3/payscore/permissions/openid/%s?appid=%s&service_id=%s' % (openid, self._appid, service_id) + else: + raise Exception('authorization_code or openid is not assigned.') + return await self._core.request(path) + + +async def payscore_permission_terminate(self, service_id, reason, authorization_code=None, openid=None, appid=None): + """解除用户授权记录(授权协议号或openid) + :param service_id: 服务ID。示例值:'500001' + :param reason: 撤销原因,解除授权原因。示例值:'撤销原因' + :param authorization_code: 授权协议号,户系统内部授权协议号,要求此参数只能由数字、大小写字母_-*组成,且在同一个商户号下唯一。示例值:'1234323JKHDFE1243252' + :param openid: 用户标识,微信用户在商户对应appid下的唯一标识。示例值:'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if not (service_id and reason): + raise Exception('service_id or reason is not assigned.') + params.update({'service_id': service_id}) + params.update({'reason': reason}) + if authorization_code: + path = 'v3/payscore/permissions/authorization-code/%s/terminate' % authorization_code + elif openid: + params.update({'appid': appid or self._appid}) + path = '/v3/payscore/permissions/openid/%s/terminate' % openid + else: + raise Exception('authorization_code or openid is not assigned.') + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def payscore_create(self, out_order_no, service_id, service_introduction, time_range, + risk_fund, attach=None, openid=None, post_payments=None, post_discounts=None, + location=None, need_user_confirm=True, notify_url=None, appid=None): + """创建支付分订单 + :param out_order_no: 商户服务订单号,商户系统内部服务订单号(不是交易单号),要求此参数只能由数字、大小写字母_-|*组成,且在同一个商户号下唯一。示例值:'1234323JKHDFE1243252' + :param service_id: 服务ID,该服务ID有本接口对应产品的权限。示例值:'500001' + :param service_introduction: 服务信息,用于介绍本订单所提供的服务 ,当参数长度超过20个字符时,报错处理。示例值:'某某酒店' + :param time_range: 服务时间范围。 + :param risk_fund: 订单风险金。 + :param attach: 商户数据包,商户数据包可存放本订单所需信息,需要先urlencode后传入。当商户数据包总长度超出256字符时,报错处理。示例值:'Easdfowealsdkjfnlaksjdlfkwqoi&wl3l2sald' + :param openid: 用户标识,微信用户在商户对应appid下的唯一标识。免确认订单:必填,需确认订单:不填。示例值:'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o' + :param post_payments: 后付费项目,后付费项目列表,最多包含100条付费项目。如果传入,用户侧则显示此参数。 + :param post_discounts: 后付费商户优惠,后付费商户优惠列表,最多包含30条商户优惠。如果传入,用户侧则显示此参数。 + :param location: 服务位置信息,如果传入,用户侧则显示此参数。 + :param need_user_confirm: 是否需要用户确认,枚举值:False:免确认订单,True:需确认订单,默认值True。示例值:True + :param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if out_order_no: + params.update({'out_order_no': out_order_no}) + else: + raise Exception('out_order_no is not assigned.') + params.update({'appid': appid or self._appid}) + if service_id: + params.update({'service_id': service_id}) + else: + raise Exception('service_id is not assigned.') + if service_introduction: + params.update({'service_introduction': service_introduction}) + else: + raise Exception('service_introduction is not assigned.') + if time_range: + params.update({'time_range': time_range}) + else: + raise Exception('time_range is not assigned.') + if risk_fund: + params.update({'risk_fund': risk_fund}) + else: + raise Exception('risk_fund is not assigned.') + if attach: + params.update({'attach': attach}) + if post_payments: + params.update({'post_payments': post_payments}) + if post_discounts: + params.update({'post_discounts': post_discounts}) + if location: + params.update({'location': location}) + params.update({'need_user_confirm': need_user_confirm}) + if not need_user_confirm: + if openid: + params.update({'openid': openid}) + else: + raise Exception('openid is not assigned.') + params.update({'notify_url': notify_url or self._notify_url}) + path = '/v3/payscore/serviceorder' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def payscore_query(self, service_id, out_order_no=None, query_id=None): + """查询支付分订单 + :param service_id: 服务ID,该服务ID有本接口对应产品的权限。示例值:'500001' + :param out_order_no: 商户服务订单号,商户系统内部服务订单号(不是交易单号),要求此参数只能由数字、大小写字母_-|*组成,且在同一个商户号下唯一。示例值:'1234323JKHDFE1243252' + :param query_id: 回跳查询ID,微信侧回跳到商户前端时用于查单的单据查询id。商户单号与回跳查询id必填其中一个。不允许都填写或都不填写。示例值:'15646546545165651651' + """ + if service_id: + path = '/v3/payscore/serviceorder?service_id=%s&appid=%s' % (service_id, self._appid) + else: + raise Exception('service_id is not assigned.') + if out_order_no: + path = '%s&out_order_no=%s' % (path, out_order_no) + elif query_id: + path = '%s&query_id=%s' % (path, query_id) + else: + raise Exception('out_order_no or query_id is not assigned.') + return await self._core.request(path) + + +async def payscore_cancel(self, out_order_no, service_id, reason, appid=None): + """取消支付分订单 + :param out_order_no: 商户服务订单号,商户系统内部服务订单号(不是交易单号),要求此参数只能由数字、大小写字母_-|*组成,且在同一个商户号下唯一。示例值:'1234323JKHDFE1243252' + :param service_id: 服务ID,该服务ID有本接口对应产品的权限。示例值:'500001' + :param query_id: 回跳查询ID,微信侧回跳到商户前端时用于查单的单据查询id。商户单号与回跳查询id必填其中一个。不允许都填写或都不填写。示例值:'15646546545165651651' + :param reason: 取消原因,最多30个字符,每个汉字/数字/英语都按1个字符计算超过长度报错处理。注:重录时需保证参数完全一致,包括取消原因。示例值:'用户投诉' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if out_order_no: + path = '/v3/payscore/serviceorder/%s/cancel' % out_order_no + else: + raise Exception('out_order_no is not assigned.') + if service_id: + params.update({'service_id': service_id}) + else: + raise Exception('service_id is not assigned.') + if reason: + params.update({'reason': reason}) + else: + raise Exception('reason is not assigned.') + params.update({'appid': appid or self._appid}) + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def payscore_modify(self, out_order_no, service_id, post_payments, total_amount, reason, post_discounts=None, appid=None): + """修改订单金额 + :param out_order_no: 商户服务订单号,商户系统内部服务订单号(不是交易单号),要求此参数只能由数字、大小写字母_-|*组成,且在同一个商户号下唯一。示例值:'1234323JKHDFE1243252' + :param service_id: 服务ID,该服务ID有本接口对应产品的权限。示例值:'500001' + :param post_payments: 后付费项目,后付费项目列表,最多包含100条付费项目。 + :param total_amount: 总金额,单位为分,不能超过完结订单时候的总金额,只能为整数,详见支付金额。示例值:50000 + :param reason: 取消原因,最多30个字符,每个汉字/数字/英语都按1个字符计算超过长度报错处理。注:重录时需保证参数完全一致,包括取消原因。示例值:'用户投诉' + :param post_discounts: 后付费商户优惠,后付费商户优惠列表,最多包含30条商户优惠。如果传入,用户侧则显示此参数。 + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if out_order_no: + path = '/v3/payscore/serviceorder/%s/modify' % out_order_no + else: + raise Exception('out_order_no is not assigned.') + if service_id: + params.update({'service_id': service_id}) + else: + raise Exception('service_id is not assigned.') + if post_payments: + params.update({'post_payments': post_payments}) + else: + raise Exception('post_payments is not assigned.') + if total_amount: + params.update({'total_amount': total_amount}) + else: + raise Exception('total_amount is not assigned.') + if reason: + params.update({'reason': reason}) + else: + raise Exception('reason is not assigned.') + if post_discounts: + params.update({'post_discounts': post_discounts}) + params.update({'appid': appid or self._appid}) + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def payscore_complete(self, out_order_no, service_id, post_payments, total_amount, post_discounts=None, + time_range=None, location=None, profit_sharing=False, goods_tag=None, appid=None): + """完结支付分订单 + :param out_order_no: 商户服务订单号,商户系统内部服务订单号(不是交易单号),要求此参数只能由数字、大小写字母_-|*组成,且在同一个商户号下唯一。示例值:'1234323JKHDFE1243252' + :param service_id: 服务ID,该服务ID有本接口对应产品的权限。示例值:'500001' + :param post_payments: 后付费项目,后付费项目列表,最多包含100条付费项目。如果传入,用户侧则显示此参数。 + :param total_amount: 总金额,数字,必须≥0(单位:分),只能为整数。示例值:100 + :param post_discounts: 后付费商户优惠,后付费商户优惠列表,最多包含30条商户优惠。如果传入,用户侧则显示此参数。 + :param time_range: 服务时间范围。 + :param location: 服务位置信息,如果传入,用户侧则显示此参数。 + :param profit_sharing: 微信支付服务分账标记,完结订单分账接口标记。False:不分账,True:分账,默认:False,示例值:False + :param goods_tag: 订单优惠标记,订单优惠标记,代金券或立减金优惠的参数,示例值:'goods_tag' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if out_order_no: + path = '/v3/payscore/serviceorder/%s/complete' % out_order_no + else: + raise Exception('out_order_no is not assigned.') + if service_id: + params.update({'service_id': service_id}) + else: + raise Exception('service_id is not assigned.') + if post_payments: + params.update({'post_payments': post_payments}) + else: + raise Exception('post_payments is not assigned.') + if type(total_amount) is int and total_amount >= 0: + params.update({'total_amount': total_amount}) + else: + raise Exception('total_amount is not assigned.') + if post_discounts: + params.update({'post_discounts': post_discounts}) + if time_range: + params.update({'time_range': time_range}) + if location: + params.update({'location': location}) + if goods_tag: + params.update({'goods_tag': goods_tag}) + params.update({'profit_sharing': profit_sharing}) + params.update({'appid': appid or self._appid}) + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def payscore_pay(self, out_order_no, service_id, appid=None): + """商户发起催收扣款 + :param out_order_no: 商户服务订单号,商户系统内部服务订单号(不是交易单号),要求此参数只能由数字、大小写字母_-|*组成,且在同一个商户号下唯一。示例值:'1234323JKHDFE1243252' + :param service_id: 服务ID,该服务ID有本接口对应产品的权限。示例值:'500001' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if out_order_no: + path = '/v3/payscore/serviceorder/%s/pay' % out_order_no + else: + raise Exception('out_order_no is not assigned.') + if service_id: + params.update({'service_id': service_id}) + else: + raise Exception('service_id is not assigned.') + params.update({'appid': appid or self._appid}) + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def payscore_sync(self, out_order_no, service_id, scene_type='Order_Paid', detail={'paid_time': None}, appid=None): + """同步服务订单信息 + :param out_order_no: 商户服务订单号,商户系统内部服务订单号(不是交易单号),要求此参数只能由数字、大小写字母_-|*组成,且在同一个商户号下唯一。示例值:'1234323JKHDFE1243252' + :param service_id: 服务ID,该服务ID有本接口对应产品的权限。示例值:'500001' + :param scene_type: 场景类型,场景类型为“Order_Paid”,表示“订单收款成功” 。示例值:'Order_Paid' + :param detail: 内容信息详情,场景类型为Order_Paid时,为必填项。其中 paid_time表示收款成功时间,示例值:'20091225091210' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if out_order_no: + path = '/v3/payscore/serviceorder/%s/sync' % out_order_no + else: + raise Exception('out_order_no is not assigned.') + if service_id: + params.update({'service_id': service_id}) + else: + raise Exception('service_id is not assigned.') + if scene_type: + params.update({'type': scene_type}) + else: + raise Exception('scene_type is not assigned.') + if detail: + params.update({'detail': detail}) + else: + raise Exception('detail is not assigned.') + params.update({'appid': appid or self._appid}) + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def payscore_refund(self, transaction_id, out_refund_no, amount, reason=None, + funds_account=None, goods_detail=None, notify_url=None): + """申请退款 + :param transaction_id: 微信支付订单号,示例值:'1217752501201407033233368018' + :param out_refund_no: 商户退款单号,示例值:'1217752501201407033233368018' + :param amount: 金额信息,示例值:{'refund':888, 'total':888, 'currency':'CNY'} + :param reason: 退款原因,示例值:'商品已售完' + :param funds_account: 退款资金来源,示例值:'AVAILABLE' + :param goods_detail: 退款商品,示例值:{'merchant_goods_id':'1217752501201407033233368018', 'wechatpay_goods_id':'1001', 'goods_name':'iPhone6s 16G', 'unit_price':528800, 'refund_amount':528800, 'refund_quantity':1} + :param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php' + """ + return refund(self, out_refund_no=out_refund_no, amount=amount, transaction_id=transaction_id, reason=reason, + funds_account=funds_account, goods_detail=goods_detail, notify_url=notify_url) + + +async def payscore_refund_query(self, out_refund_no): + """查询单笔退款 + :param out_refund_no: 商户退款单号,示例值:'1217752501201407033233368018' + """ + return query_refund(self, out_refund_no=out_refund_no) + + +async def payscore_merchant_bill(self, bill_date, service_id, tar_type='GZIP', encryption_algorithm='AEAD_AES_256_GCM'): + """商户申请获取对账单 + :param bill_date: 账单日期,格式'YYYY-MM-DD',仅支持下载近三个月的账单。示例值:'2021-01-01' + :param service_id: 支付分服务ID。示例值:'2002000000000558128851361561536' + :param tar_type: 账单的压缩类型,'GZIP':文件压缩方式为gzip,返回.gzip格式的压缩文件。示例值:'GZIP' + :param encryption_algorithm: 加密算法,对返回账单原文加密的算法'AEAD_AES_256_GCM',账单使用AEAD_AES_256_GCM加密算法进行加密。示例值:'AEAD_AES_256_GCM' + """ + if bill_date: + path = '/v3/payscore/merchant-bill?bill_date=%s' % bill_date + else: + raise Exception('bill_date is not assigned.') + if service_id: + path = '%s&service_id=%s' % (path, service_id) + else: + raise Exception('service_id is not assigned.') + path = '%s&tar_type=%s' % (path, tar_type if tar_type else 'GZIP') + path = '%s&encryption_algorithm=%s' % (path, encryption_algorithm if encryption_algorithm else 'AEAD_AES_256_GCM') + return await self._core.request(path) diff --git a/wechatpayv3/async_/profitsharing.py b/wechatpayv3/async_/profitsharing.py new file mode 100644 index 0000000..f00ce16 --- /dev/null +++ b/wechatpayv3/async_/profitsharing.py @@ -0,0 +1,518 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType + + +async def profitsharing_order(self, transaction_id, out_order_no, receivers, unfreeze_unsplit, + appid=None, sub_appid=None, sub_mchid=None): + """请求分账 + :param transaction_id: 微信支付订单号,示例值:'4208450740201411110007820472' + :param out_order_no: 商户分账单号,只能是数字、大小写字母_-|*@,示例值:'P20150806125346' + :param receivers: 分账接收方列表,最多可有50个分账接收方,示例值:[{'type':'MERCHANT_ID', 'account':'86693852', 'amount':888, 'description':'分给商户A'}] + :param unfreeze_unsplit: 是否解冻剩余未分资金,示例值:True, False + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param sub_appid: (服务商模式)子商户应用ID,示例值:'wxd678efh567hg6999' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if transaction_id: + params.update({'transaction_id': transaction_id}) + else: + raise Exception('transaction_id is not assigned') + if out_order_no: + params.update({'out_order_no': out_order_no}) + else: + raise Exception('out_order_no is not assigned') + if isinstance(unfreeze_unsplit, bool): + params.update({'unfreeze_unsplit': unfreeze_unsplit}) + else: + raise Exception('unfreeze_unsplit is not assigned') + if isinstance(receivers, list): + params.update({'receivers': receivers}) + else: + raise Exception('receivers is not assigned') + cipher_data = False + for receiver in params.get('receivers'): + if receiver.get('name'): + receiver['name'] = self._core.encrypt(receiver.get('name')) + cipher_data = True + params.update({'appid': appid or self._appid}) + if self._partner_mode: + if sub_appid: + params.update({'sub_appid': sub_appid}) + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + path = '/v3/profitsharing/orders' + return await self._core.request(path, method=RequestType.POST, data=params, cipher_data=cipher_data) + + +async def profitsharing_order_query(self, transaction_id, out_order_no, sub_mchid=None): + """查询分账结果 + :param transaction_id: 微信支付订单号,示例值:'4208450740201411110007820472' + :param out_order_no: 商户分账单号,只能是数字、大小写字母_-|*@,示例值:'P20150806125346' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + if transaction_id and out_order_no: + path = '/v3/profitsharing/orders/%s?transaction_id=%s' % (out_order_no, transaction_id) + else: + raise Exception('transaction_id or out_order_no is not assigned.') + if self._partner_mode: + if sub_mchid: + path = '%s&sub_mchid=%s' % (path, sub_mchid) + else: + raise Exception('sub_mchid is not assigned.') + return await self._core.request(path) + + +async def profitsharing_return(self, out_return_no, return_mchid, amount, description, + order_id=None, out_order_no=None, sub_mchid=None): + """请求分账回退 + :param out_return_no: 商户回退单号,商户在自己后台生成的一个新的回退单号,在商户后台唯一,示例值:'R20190516001' + :param return_mchid: 回退商户号,分账接口中的分账接收方商户号,示例值:'86693852' + :param amount: 回退金额,单位为分,示例值:888 + :param description: 回退描述,分账回退的原因描述,示例值:'用户退款' + :param order_id: 微信分账单号,与out_order_no参数二选一,示例值:'3008450740201411110007820472' + :param out_order_no: 商户分账单号,只能是数字、大小写字母_-|*@,示例值:'P20150806125346' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if order_id: + params.update({'order_id': order_id}) + elif out_order_no: + params.update({'out_order_no': out_order_no}) + else: + raise Exception('order_id or out_order_no is not assigned.') + if out_return_no: + params.update({'out_return_no': out_return_no}) + else: + raise Exception('out_return_no is not assigned') + if return_mchid: + params.update({'return_mchid': return_mchid}) + else: + raise Exception('return_mchid is not assigned') + if amount: + params.update({'amount': amount}) + else: + raise Exception('amount is not assigned') + if description: + params.update({'description': description}) + else: + raise Exception('description is not assigned') + if self._partner_mode: + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + path = '/v3/profitsharing/return-orders' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def profitsharing_return_query(self, out_order_no, out_return_no, sub_mchid=None): + """查询分账回退结果 + :param out_order_no: 商户分账单号,只能是数字、大小写字母_-|*@,示例值:'P20150806125346' + :param out_return_no: 商户回退单号,商户在自己后台生成的一个新的回退单号,在商户后台唯一,示例值:'R20190516001' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + if out_order_no and out_return_no: + path = '/v3/profitsharing/return-orders/%s?&out_order_no=%s' % (out_return_no, out_order_no) + else: + raise Exception('out_order_no or out_return_no is not assigned') + if self._partner_mode: + if sub_mchid: + path = '%s&sub_mchid=%s' % (path, sub_mchid) + else: + raise Exception('sub_mchid is not assigned.') + return await self._core.request(path) + + +async def profitsharing_unfreeze(self, transaction_id, out_order_no, description, sub_mchid=None): + """解冻剩余资金 + :param transaction_id: 微信支付订单号,示例值:'4208450740201411110007820472' + :param out_order_no: 商户分账单号,只能是数字、大小写字母_-|*@,示例值:'P20150806125346' + :param description: 分账描述,分账的原因描述,分账账单中需要体现,示例值:'解冻全部剩余资金' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if transaction_id: + params.update({'transaction_id': transaction_id}) + else: + raise Exception('transaction_id is not assigned') + if out_order_no: + params.update({'out_order_no': out_order_no}) + else: + raise Exception('out_order_no is not assigned') + if description: + params.update({'description': description}) + else: + raise Exception('description is not assigned') + if self._partner_mode: + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + path = '/v3/profitsharing/orders/unfreeze' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def profitsharing_amount_query(self, transaction_id): + """查询剩余待分金额 + :param transaction_id: 微信支付订单号,示例值:'4208450740201411110007820472' + """ + if transaction_id: + path = '/v3/profitsharing/transactions/%s/amounts' % transaction_id + else: + raise Exception('transaction_id is not assigned') + return await self._core.request(path) + + +async def profitsharing_config_query(self, sub_mchid): + """查询最大分账比例 + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + if sub_mchid: + path = '/v3/profitsharing/merchant-configs/%s' % sub_mchid + else: + raise Exception('sub_mchid is not assigned') + return await self._core.request(path) + + +async def profitsharing_add_receiver(self, account_type, account, relation_type, name=None, + custom_relation=None, appid=None, sub_appid=None, sub_mchid=None): + """添加分账接收方 + :param account_type: 分账接收方类型,枚举值:'MERCHANT_ID':商户ID,'PERSONAL_OPENID':个人openid + :param account: 分账接收方账号,类型是'MERCHANT_ID'时,是商户号,类型是'PERSONAL_OPENID'时,是个人openid,示例值:'86693852' + :param relation_type:与分账方的关系类型,枚举值:'STORE':门店,'STAFF':员工,'STORE_OWNER':店主, + 'PARTNER':合作伙伴,'HEADQUARTER':总部,'BRAND':品牌方,'DISTRIBUTOR':分销商, + 'USER':用户,'SUPPLIER': 供应商,'CUSTOM':自定义,示例值:'STORE' + :param name: 分账个人接收方姓名,分账接收方类型是'MERCHANT_ID'时,是商户全称(必传),当商户是小微商户或个体户时,是开户人姓名, + 分账接收方类型是'PERSONAL_OPENID'时,是个人姓名 + :param custom_relation: 自定义的分账关系,子商户与接收方具体的关系,本字段最多10个字。当字段'relation_type'的值为'CUSTOM'时,本字段必填; + 当字段'relation_type'的值不为'CUSTOM'时,本字段无需填写。示例值:'代理商' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param sub_appid: (服务商模式)子商户应用ID,示例值:'wxd678efh567hg6999' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if account_type: + params.update({'type': account_type}) + else: + raise Exception('account_type is not assigned') + if account: + params.update({'account': account}) + else: + raise Exception('account is not assigned') + if relation_type: + params.update({'relation_type': relation_type}) + else: + raise Exception('relation_type is not assigned') + cipher_data = False + if name: + params.update({'name': self._core.encrypt(name)}) + cipher_data = True + if relation_type == 'CUSTOM': + if custom_relation: + params.update({'custom_relation': custom_relation}) + else: + raise Exception('custom_relation is not assigned') + params.update({'appid': appid or self._appid}) + if self._partner_mode: + if sub_appid: + params.update({'sub_appid': sub_appid}) + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + path = '/v3/profitsharing/receivers/add' + return await self._core.request(path, method=RequestType.POST, data=params, cipher_data=cipher_data) + + +async def profitsharing_delete_receiver(self, account_type, account, appid=None, sub_appid=None, sub_mchid=None): + """删除分账接收方 + :param account_type: 分账接收方类型,枚举值:'MERCHANT_ID':商户ID,'PERSONAL_OPENID':个人openid + :param account: 分账接收方账号,类型是'MERCHANT_ID'时,是商户号,类型是'PERSONAL_OPENID'时,是个人openid,示例值:'86693852' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param sub_appid: (服务商模式)子商户应用ID,示例值:'wxd678efh567hg6999' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if account_type: + params.update({'type': account_type}) + else: + raise Exception('account_type is not assigned') + if account: + params.update({'account': account}) + else: + raise Exception('account is not assigned') + params.update({'appid': appid or self._appid}) + if self._partner_mode: + if sub_appid: + params.update({'sub_appid': sub_appid}) + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + path = '/v3/profitsharing/receivers/delete' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def profitsharing_bill(self, bill_date, tar_type='GZIP', sub_mchid=None): + """申请分账账单 + :param bill_date: 账单日期,格式'YYYY-MM-DD',仅支持三个月内的账单下载申请。示例值:'2019-06-11' + :param tar_type: 压缩类型,默认值:'GZIP' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + path = '/v3/profitsharing/bills?bill_date=%s&tar_type=%s' % (bill_date, tar_type) + if self._partner_mode and sub_mchid: + path = '%s&sub_mchid=%s' % (path, sub_mchid) + return await self._core.request(path) + + +async def brand_profitsharing_order(self, brand_mchid, sub_mchid, transaction_id, out_order_no, receivers, + finish, appid=None, sub_appid=None): + """连锁品牌请求分账 + :param brand_mchid: 品牌主商户号,示例值:'1900000108' + :param sub_mchid: 子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + :param transaction_id: 微信支付订单号,示例值:'4208450740201411110007820472' + :param out_order_no: 商户分账单号,只能是数字、大小写字母_-|*@,示例值:'P20150806125346' + :param receivers: 分账接收方列表,最多可有50个分账接收方,示例值:{{'type':'MERCHANT_ID', 'account':'86693852', 'amount':888, 'description':'分给商户A'}} + :param finish: 是否完成分账,示例值:True, False + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param sub_appid: 子商户应用ID,示例值:'wxd678efh567hg6999' + """ + params = {} + if brand_mchid: + params.update({'brand_mchid': brand_mchid}) + else: + raise Exception('brand_mchid is not assigned') + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + if transaction_id: + params.update({'transaction_id': transaction_id}) + else: + raise Exception('transaction_id is not assigned') + if out_order_no: + params.update({'out_order_no': out_order_no}) + else: + raise Exception('out_order_no is not assigned') + if receivers: + params.update({'receivers': receivers}) + else: + raise Exception('receivers is not assigned') + if isinstance(finish, bool): + params.update({'finish': finish}) + else: + raise Exception('finish is not assigned') + params.update({'appid': appid or self._appid}) + if sub_appid: + params.update({'sub_appid': sub_appid}) + path = '/v3/brand/profitsharing/orders' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def brand_profitsharing_order_query(self, transaction_id, out_order_no, sub_mchid): + """查询连锁品牌分账结果 + :param transaction_id: 微信支付订单号,示例值:'4208450740201411110007820472' + :param out_order_no: 商户分账单号,只能是数字、大小写字母_-|*@,示例值:'P20150806125346' + :param sub_mchid: 子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + if sub_mchid: + path = '/v3/brand/profitsharing/orders?sub_mchid=%s' % sub_mchid + else: + raise Exception('sub_mchid is not assigned.') + if transaction_id and out_order_no: + path = '%s&transaction_id=%s&out_order_no=%s' % (path, transaction_id, out_order_no) + else: + raise Exception('transaction_id or out_order_no is not assigned.') + return await self._core.request(path) + + +async def brand_profitsharing_return(self, sub_mchid, out_return_no, return_mchid, amount, + description, order_id=None, out_order_no=None,): + """请求连锁品牌分账回退 + :param sub_mchid: 子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + :param out_return_no: 商户回退单号,商户在自己后台生成的一个新的回退单号,在商户后台唯一,示例值:'R20190516001' + :param return_mchid: 回退商户号,分账接口中的分账接收方商户号,示例值:'86693852' + :param amount: 回退金额,单位为分,示例值:888 + :param description: 回退描述,分账回退的原因描述,示例值:'用户退款' + :param order_id: 微信分账单号,与out_order_no参数二选一,示例值:'3008450740201411110007820472' + :param out_order_no: 商户分账单号,只能是数字、大小写字母_-|*@,示例值:'P20150806125346' + """ + params = {} + if not (order_id and out_order_no): + raise Exception('order_id or out_order_no is not assigned') + if order_id: + params.update({'order_id': order_id}) + elif out_order_no: + params.update({'out_order_no': out_order_no}) + else: + raise Exception('order_id or out_order_no is not assigned.') + if out_return_no: + params.update({'out_return_no': out_return_no}) + else: + raise Exception('out_return_no is not assigned') + if return_mchid: + params.update({'return_mchid': return_mchid}) + else: + raise Exception('return_mchid is not assigned') + if amount: + params.update({'amount': amount}) + else: + raise Exception('amount is not assigned') + if description: + params.update({'description': description}) + else: + raise Exception('description is not assigned') + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + path = '/v3/brand/profitsharing/returnorders' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def brand_profitsharing_return_query(self, sub_mchid, out_return_no, order_id=None, out_order_no=None): + """查询连锁品牌分账回退结果 + :param sub_mchid: 子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + :param out_return_no: 商户回退单号,商户在自己后台生成的一个新的回退单号,在商户后台唯一,示例值:'R20190516001' + :param order_id: 微信分账单号,与out_order_no参数二选一,示例值:'3008450740201411110007820472' + :param out_order_no: 商户分账单号,只能是数字、大小写字母_-|*@,示例值:'P20150806125346' + """ + if sub_mchid: + path = '/v3/brand/profitsharing/returnorders?sub_mchid=%s' % sub_mchid + else: + raise Exception('sub_mchid is not assigned.') + if out_return_no: + path = '%s&out_return_no=%s' % (path, out_return_no) + else: + raise Exception('out_return_no is not assigned') + if order_id: + path = '%s&order_id=%s' % (path, order_id) + elif out_order_no: + path = '%s&out_order_no=%s' % (path, out_order_no) + else: + raise Exception('order_id or out_order_no is not assigned.') + return await self._core.request(path) + + +async def brand_profitsharing_unfreeze(self, sub_mchid, transaction_id, out_order_no, description): + """完结连锁品牌分账 + :param sub_mchid: 子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + :param transaction_id: 微信支付订单号,示例值:'4208450740201411110007820472' + :param out_order_no: 商户分账单号,只能是数字、大小写字母_-|*@,示例值:'P20150806125346' + :param description: 分账描述,分账的原因描述,分账账单中需要体现,示例值:'解冻全部剩余资金' + """ + params = {} + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + if transaction_id: + params.update({'transaction_id': transaction_id}) + else: + raise Exception('transaction_id is not assigned') + if out_order_no: + params.update({'out_order_no': out_order_no}) + else: + raise Exception('out_order_no is not assigned') + if description: + params.update({'description': description}) + else: + raise Exception('description is not assigned') + path = '/v3/brand/profitsharing/finish-order' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def brand_profitsharing_amount_query(self, transaction_id): + """查询连锁品牌分账剩余待分金额 + :param transaction_id: 微信支付订单号,示例值:'4208450740201411110007820472' + """ + if transaction_id: + path = '/v3/brand/profitsharing/orders/%s/amounts' % transaction_id + else: + raise Exception('transaction_id is not assigned') + return await self._core.request(path) + + +async def brand_profitsharing_config_query(self, brand_mchid): + """查询连锁品牌分账最大分账比例 + :param brand_mchid: 品牌商户号,示例值:'1900000108' + """ + if brand_mchid: + path = '/v3/brand/profitsharing/brand-configs/%s' % brand_mchid + else: + raise Exception('brand_mchid is not assigned') + return await self._core.request(path) + + +async def brand_profitsharing_add_receiver(self, brand_mchid, account_type, account, relation_type, + name=None, appid=None, sub_appid=None): + """添加分账接收方 + :param brand_mchid: 品牌商户号,示例值:'1900000108' + :param account_type: 分账接收方类型,枚举值:'MERCHANT_ID':商户ID,'PERSONAL_OPENID':个人openid + :param account: 分账接收方账号,类型是'MERCHANT_ID'时,是商户号,类型是'PERSONAL_OPENID'时,是个人openid,示例值:'86693852' + :param relation_type:与分账方的关系类型,枚举值:'STORE':门店,'STAFF':员工,'STORE_OWNER':店主, + 'PARTNER':合作伙伴,'HEADQUARTER':总部,'BRAND':品牌方,'DISTRIBUTOR':分销商, + 'USER':用户,'SUPPLIER': 供应商,'CUSTOM':自定义,示例值:'STORE' + :param name: 分账个人接收方姓名,分账接收方类型是'MERCHANT_ID'时,是商户全称(必传),当商户是小微商户或个体户时,是开户人姓名, + 分账接收方类型是'PERSONAL_OPENID'时,是个人姓名 + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param sub_appid: 子商户应用ID,示例值:'wxd678efh567hg6999' + """ + params = {} + if brand_mchid: + params.update({'brand_mchid': brand_mchid}) + else: + raise Exception('brand_mchid is not assigned.') + if account_type: + params.update({'type': account_type}) + else: + raise Exception('account_type is not assigned') + if account: + params.update({'account': account}) + else: + raise Exception('account is not assigned') + if relation_type: + params.update({'relation_type': relation_type}) + else: + raise Exception('relation_type is not assigned') + cipher_data = False + if name: + params.update({'name': self._core.encrypt(name)}) + cipher_data = True + params.update({'appid': appid or self._appid}) + if sub_appid: + params.update({'sub_appid': sub_appid}) + path = '/v3/brand/profitsharing/receivers/add' + return await self._core.request(path, method=RequestType.POST, data=params, cipher_data=cipher_data) + + +async def brand_profitsharing_delete_receiver(self, brand_mchid, account_type, account, appid=None, sub_appid=None): + """删除连锁品牌分账接收方 + :param brand_mchid: 品牌商户号,示例值:'1900000108' + :param account_type: 分账接收方类型,枚举值:'MERCHANT_ID':商户ID,'PERSONAL_OPENID':个人openid + :param account: 分账接收方账号,类型是'MERCHANT_ID'时,是商户号,类型是'PERSONAL_OPENID'时,是个人openid,示例值:'86693852' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param sub_appid: (服务商模式)子商户应用ID,示例值:'wxd678efh567hg6999' + """ + params = {} + if brand_mchid: + params.update({'brand_mchid': brand_mchid}) + else: + raise Exception('brand_mchid is not assigned.') + if account_type: + params.update({'type': account_type}) + else: + raise Exception('account_type is not assigned') + if account: + params.update({'account': account}) + else: + raise Exception('account is not assigned') + params.update({'appid': appid or self._appid}) + if sub_appid: + params.update({'sub_appid': sub_appid}) + path = '/v3/profitsharing/receivers/delete' + return await self._core.request(path, method=RequestType.POST, data=params) diff --git a/wechatpayv3/async_/smartguide.py b/wechatpayv3/async_/smartguide.py new file mode 100644 index 0000000..63b2ee4 --- /dev/null +++ b/wechatpayv3/async_/smartguide.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType + + +async def guides_register(self, corpid, store_id, userid, name, mobile, qr_code, avatar, group_qrcode=None, sub_mchid=None): + """服务人员注册 + :param corpid: 企业ID, 示例值:'1234567890' + :param store_id: 门店ID, 示例值:12345678 + :param userid: 企业微信的员工ID, 示例值:'robert' + :param name: 企业微信的员工姓名, 示例值:'robert' + :param mobile: 手机号码, 示例值:'13900000000' + :param qr_code: 员工个人二维码, 示例值:'https://open.work.weixin.qq.com/wwopen/userQRCode?vcode=xxx' + :param avatar: 头像URL, 示例值:'http://wx.qlogo.cn/mmopen/ajNVdqHZLLA3WJ6DSZUfiakYe37PKnQhBIeOQBO4czqrnZDS79FH5Wm5m4X69TBicnHFlhiafvDwklOpZeXYQQ2icg/0' + :param group_qrcode: 群二维码URL, 示例值:'http://p.qpic.cn/wwhead/nMl9ssowtibVGyrmvBiaibzDtp/0' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if corpid: + params.update({'corpid': corpid}) + else: + raise Exception('corpid is not assigned.') + if store_id: + params.update({'store_id': store_id}) + else: + raise Exception('store_id is not assigned.') + if userid: + params.update({'userid': userid}) + else: + raise Exception('userid is not assigned.') + if name: + params.update({'name': self._core.encrypt(name)}) + else: + raise Exception('name is not assigned') + if mobile: + params.update({'mobile': self._core.encrypt(mobile)}) + else: + raise Exception('mobile is not assigned.') + if qr_code: + params.update({'qr_code': qr_code}) + else: + raise Exception('qr_code is not assigned.') + if avatar: + params.update({'avatar': avatar}) + else: + raise Exception('avatar is not assigned.') + if group_qrcode: + params.update({'group_qrcode': group_qrcode}) + if self._partner_mode and sub_mchid: + params.update({'sub_mchid': sub_mchid}) + path = '/v3/smartguide/guides' + return await self._core.request(path, method=RequestType.POST, data=params, cipher_data=True) + + +async def guides_assign(self, guide_id, out_trade_no, sub_mchid=None): + """服务人员分配 + :param guide_id: 服务人员ID,示例值:'LLA3WJ6DSZUfiaZDS79FH5Wm5m4X69TBic' + :param out_trade_no: 商户订单号, 示例值:'20150806125346' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if out_trade_no: + params.update({'out_trade_no': out_trade_no}) + else: + raise Exception('out_trade_no is not assigned.') + if self._partner_mode and sub_mchid: + params.update({'sub_mchid': sub_mchid}) + if guide_id: + path = '/v3/smartguide/guides/%s/assign' % guide_id + else: + raise Exception('guide_id is not assigned.') + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def guides_query(self, store_id, userid=None, mobile=None, work_id=None, limit=None, offset=0, sub_mchid=None): + """服务人员查询 + :params store_id: 门店ID, 示例值:1234 + :params userid: 企业微信的员工ID, 示例值:'robert' + :params mobile: 手机号码, 示例值:'13900000000' + :params work_id: 工号, 示例值:'robert' + :params limit: 最大资源条数, 示例值:5 + :params offset: 请求资源起始位置, 示例值:0 + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + if not store_id: + raise Exception('store_id is not assigned.') + path = '/v3/smartguide/guides?store_id=%s' % store_id + if userid: + path = '%s&userid=%s' % (path, userid) + cipher_data = False + if mobile: + path = '%s&mobile=%s' % (path, self._core.encrypt(mobile)) + cipher_data = True + if work_id: + path = '%s&work_id=%s' % (path, work_id) + if limit: + path = '%s&limit=%s' % (path, limit) + if offset: + path = '%s&offset=%s' % (path, offset) + if self._partner_mode and sub_mchid: + path = '%s&sub_mchid=%s' % (path, sub_mchid) + return await self._core.request(path, cipher_data=cipher_data) + + +async def guides_update(self, guide_id, name=None, mobile=None, qr_code=None, avatar=None, group_qrcode=None, sub_mchid=None): + """服务人员信息更新 + :params guide_id: 服务人员ID, 示例值:'LLA3WJ6DSZUfiaZDS79FH5Wm5m4X69TBic' + :params name: 服务人员姓名, 示例值:'robert' + :params mobile: 服务人员手机号码, 示例值:'13900000000' + :params qr_code: 服务人员二维码URL, 示例值:'https://open.work.weixin.qq.com/wwopen/userQRCode?vcode=xxx' + :params avatar: 服务人员头像URL, 示例值:'http://wx.qlogo.cn/mmopen/ajNVdqHZLLA3WJ6DSZUfiakYe37PKnQhBIeOQBO4czqrnZDS79FH5Wm5m4X69TBicnHFlhiafvDwklOpZeXYQQ2icg/0' + :params group_qrcode: 群二维码URL, 示例值:'http://p.qpic.cn/wwhead/nMl9ssowtibVGyrmvBiaibzDtp/0' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if not guide_id: + raise Exception('guide_id is not assigned.') + path = '/v3/smartguide/guides/%s' % guide_id + cipher_data = False + if name: + params.update({'name': self._core.encrypt(name)}) + cipher_data = True + if mobile: + params.update({'mobile': self._core.encrypt(mobile)}) + cipher_data = True + if qr_code: + params.update({'qr_code': qr_code}) + if avatar: + params.update({'avatar': avatar}) + if group_qrcode: + params.update({'group_qrcode': group_qrcode}) + if self._partner_mode and sub_mchid: + params.update({'sub_mchid': sub_mchid}) + return await self._core.request(path, method=RequestType.PATCH, data=params, cipher_data=cipher_data) diff --git a/wechatpayv3/async_/transaction.py b/wechatpayv3/async_/transaction.py new file mode 100644 index 0000000..0b32001 --- /dev/null +++ b/wechatpayv3/async_/transaction.py @@ -0,0 +1,458 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType, WeChatPayType + + +async def pay(self, + description, + out_trade_no, + amount, + payer=None, + time_expire=None, + attach=None, + goods_tag=None, + detail=None, + scene_info=None, + settle_info=None, + notify_url=None, + appid=None, + mchid=None, + sub_appid=None, + sub_mchid=None, + support_fapiao=False, + pay_type=None): + """统一下单 + :return code, message: + :param description: 商品描述,示例值:'Image形象店-深圳腾大-QQ公仔' + :param out_trade_no: 商户订单号,示例值:'1217752501201407033233368018' + :param amount: 订单金额,示例值:{'total':100, 'currency':'CNY'} + :param payer: 支付者,示例值:{'openid':'oHkLxtx0vUqe-18p_AXTZ1innxkCY'} + :param time_expire: 交易结束时间,示例值:'2018-06-08T10:34:56+08:00' + :param attach: 附加数据,示例值:'自定义数据' + :param goods_tag: 订单优惠标记,示例值:'WXG' + :param detail: 优惠功能,示例值:{'cost_price':608800, 'invoice_id':'微信123', 'goods_detail':[{'merchant_goods_id':'商品编码', 'wechatpay_goods_id':'1001', 'goods_name':'iPhoneX 256G', 'quantity':1, 'unit_price':828800}]} + :param scene_info: 场景信息,示例值:{'payer_client_ip':'14.23.150.211', 'device_id':'013467007045764', 'store_info':{'id':'0001', 'name':'腾讯大厦分店', 'area_code':'440305', 'address':'广东省深圳市南山区科技中一道10000号'}} + :param settle_info: 结算信息,示例值:{'profit_sharing':False} + :param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param mchid: 微信支付商户号,可不填,默认传入初始化的mchid,示例值:'987654321' + :param sub_appid: (服务商模式)子商户应用ID,示例值:'wxd678efh567hg6999' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + :param support_fapiao: 电子发票入口开放标识,传入true时,支付成功消息和支付详情页将出现开票入口。 + :param pay_type: 微信支付类型,示例值:WeChatPayType.JSAPI + """ + params = {} + if pay_type != WeChatPayType.CODEPAY: + if not (notify_url or self._notify_url): + raise Exception('notify_url is not assigned.') + params.update({'notify_url': notify_url or self._notify_url}) + if description: + params.update({'description': description}) + else: + raise Exception('description is not assigned.') + if out_trade_no: + params.update({'out_trade_no': out_trade_no}) + else: + raise Exception('out_trade_no is not assigned.') + if amount: + params.update({'amount': amount}) + else: + raise Exception('amount is not assigned.') + if payer: + params.update({'payer': payer}) + if scene_info: + params.update({'scene_info': scene_info}) + if time_expire: + params.update({'time_expire': time_expire}) + if attach: + params.update({'attach': attach}) + if goods_tag: + params.update({'goods_tag': goods_tag}) + if detail: + params.update({'detail': detail}) + if settle_info: + params.update({'settle_info': settle_info}) + pay_type = pay_type or self._type + if self._partner_mode: + params.update({'sp_appid': appid or self._appid}) + params.update({'sp_mchid': mchid or self._mchid}) + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + if sub_appid: + params.update({'sub_appid': sub_appid}) + if pay_type in [WeChatPayType.JSAPI, WeChatPayType.MINIPROG]: + if not payer: + raise Exception('payer is not assigned') + path = '/v3/pay/partner/transactions/jsapi' + elif pay_type == WeChatPayType.APP: + path = '/v3/pay/partner/transactions/app' + elif pay_type == WeChatPayType.H5: + if not scene_info: + raise Exception('scene_info is not assigned.') + path = '/v3/pay/partner/transactions/h5' + elif pay_type == WeChatPayType.NATIVE: + path = '/v3/pay/partner/transactions/native' + elif pay_type == WeChatPayType.CODEPAY: + path = '/v3/pay/partner/transactions/codepay' + else: + raise Exception('pay_type is not assigned.') + else: + params.update({'appid': appid or self._appid}) + params.update({'mchid': mchid or self._mchid}) + if pay_type in [WeChatPayType.JSAPI, WeChatPayType.MINIPROG]: + if not payer: + raise Exception('payer is not assigned') + path = '/v3/pay/transactions/jsapi' + elif pay_type == WeChatPayType.APP: + path = '/v3/pay/transactions/app' + elif pay_type == WeChatPayType.H5: + if not scene_info: + raise Exception('scene_info is not assigned.') + path = '/v3/pay/transactions/h5' + elif pay_type == WeChatPayType.NATIVE: + path = '/v3/pay/transactions/native' + elif pay_type == WeChatPayType.CODEPAY: + path = '/v3/pay/transactions/codepay' + else: + raise Exception('pay_type is not assigned.') + if support_fapiao: + params.update({'support_fapiao': support_fapiao}) + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def close(self, out_trade_no, mchid=None, sub_mchid=None): + """关闭订单 + :param out_trade_no: 商户订单号,示例值:'1217752501201407033233368018' + :param mchid: 微信支付商户号,可不传,默认传入初始化的mchid。示例值:'987654321' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + if self._partner_mode: + if out_trade_no: + path = '/v3/pay/partner/transactions/out-trade-no/%s/close' % out_trade_no + else: + raise Exception('out_trade_no is not assigned.') + if sub_mchid: + params = {'sp_mchid': mchid or self._mchid, 'sub_mchid': sub_mchid} + else: + raise Exception('sub_mchid is not assigned.') + else: + if out_trade_no: + path = '/v3/pay/transactions/out-trade-no/%s/close' % out_trade_no + else: + raise Exception('out_trade_no is not assigned.') + params = {'mchid': mchid or self._mchid} + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def query(self, transaction_id=None, out_trade_no=None, mchid=None, sub_mchid=None): + """查询订单 + :param transaction_id: 微信支付订单号,示例值:1217752501201407033233368018 + :param out_trade_no: 商户订单号,示例值:1217752501201407033233368018 + :param mchid: 微信支付商户号,可不传,默认传入初始化的mchid。示例值:'987654321' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + if self._partner_mode: + if transaction_id: + path = '/v3/pay/partner/transactions/id/%s' % transaction_id + elif out_trade_no: + path = '/v3/pay/partner/transactions/out-trade-no/%s' % out_trade_no + else: + raise Exception('transaction_id or out_trade_no is not assigned.') + path = '%s?sp_mchid=%s&sub_mchid=%s' % (path, mchid or self._mchid, sub_mchid) + else: + if transaction_id: + path = '/v3/pay/transactions/id/%s' % transaction_id + elif out_trade_no: + path = '/v3/pay/transactions/out-trade-no/%s' % out_trade_no + else: + raise Exception('transaction_id out_trade_no is not assigned.') + path = '%s?mchid=%s' % (path, mchid or self._mchid) + return await self._core.request(path) + + +async def refund(self, + out_refund_no, + amount, + transaction_id=None, + out_trade_no=None, + reason=None, + funds_account=None, + goods_detail=None, + notify_url=None, + sub_mchid=None): + """申请退款 + :param out_refund_no: 商户退款单号,示例值:'1217752501201407033233368018' + :param amount: 金额信息,示例值:{'refund':888, 'total':888, 'currency':'CNY'} + :param transaction_id: 微信支付订单号,示例值:'1217752501201407033233368018' + :param out_trade_no: 商户订单号,示例值:'1217752501201407033233368018' + :param reason: 退款原因,示例值:'商品已售完' + :param funds_account: 退款资金来源,示例值:'AVAILABLE' + :param goods_detail: 退款商品,示例值:{'merchant_goods_id':'1217752501201407033233368018', 'wechatpay_goods_id':'1001', 'goods_name':'iPhone6s 16G', 'unit_price':528800, 'refund_amount':528800, 'refund_quantity':1} + :param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if notify_url or self._notify_url: + params.update({'notify_url': notify_url or self._notify_url}) + if out_refund_no: + params.update({'out_refund_no': out_refund_no}) + else: + raise Exception('out_refund_no is not assigned.') + if amount: + params.update({'amount': amount}) + else: + raise Exception('amount is not assigned.') + if transaction_id: + params.update({'transaction_id': transaction_id}) + elif out_trade_no: + params.update({'out_trade_no': out_trade_no}) + else: + raise Exception('transaction_id is not assigned.') + if reason: + params.update({'reason': reason}) + if funds_account: + params.update({'funds_account': funds_account}) + if goods_detail: + params.update({'goods_detail': goods_detail}) + if self._partner_mode: + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + path = '/v3/refund/domestic/refunds' + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def query_refund(self, out_refund_no, sub_mchid=None): + """查询单笔退款 + :param out_refund_no: 商户退款单号,示例值:'1217752501201407033233368018' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + path = '/v3/refund/domestic/refunds/%s' % out_refund_no + if self._partner_mode: + if sub_mchid: + path = '%s?sub_mchid=%s' % (path, sub_mchid) + else: + raise Exception('sub_mchid is not assigned.') + return await self._core.request(path) + + +async def trade_bill(self, bill_date, bill_type='ALL', tar_type='GZIP', sub_mchid=None): + """申请交易账单 + :param bill_date: 账单日期,示例值:'2019-06-11' + :param bill_type: 账单类型, 默认值:'ALL' + :param tar_type: 压缩类型,默认值:'GZIP' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + path = '/v3/bill/tradebill?bill_date=%s&bill_type=%s&tar_type=%s' % (bill_date, bill_type, tar_type) + if self._partner_mode and sub_mchid: + path = '%s&sub_mchid=%s' % (path, sub_mchid) + return await self._core.request(path) + + +async def fundflow_bill(self, bill_date, account_type='BASIC', tar_type='GZIP'): + """申请资金账单 + :param bill_date: 账单日期,示例值:'2019-06-11' + :param account_type: 资金账户类型, 默认值:'BASIC',基本账户, 可选:'OPERATION',运营账户;'FEES',手续费账户 + :param tar_type: 压缩类型,默认值:'GZIP' + """ + if not bill_date: + raise Exception('bill_date is not assigned.') + path = '/v3/bill/fundflowbill?bill_date=%s&account_type=%s&tar_type=%s' % (bill_date, account_type, tar_type) + return await self._core.request(path) + + +async def submch_fundflow_bill(self, sub_mchid, bill_date, account_type, algorithm='AEAD_AES_256_GCM', tar_type=None): + """申请单个子商户资金账单 + :param sub_mchid: 子商户号,示例值:'19000000001' + :param bill_date: 账单日期,格式YYYY-MM-DD,示例值:'2019-06-11' + :param account_type: 资金账户类型,枚举值:'BASIC':基本账户,'OPERATION':运营账户,'FEES':手续费账户,示例值:'BASIC' + :param algorithm: 加密算法,枚举值:'AEAD_AES_256_GCM':AEAD_AES_256_GCM加密算法 + :param tar_type: 压缩格式,枚举值:'GZIP':返回格式为.gzip的压缩包账单 + """ + path = '/v3/bill/sub-merchant-fundflowbill' + if sub_mchid: + path += '?sub_mchid=%s' % sub_mchid + else: + raise Exception('sub_mchid is not assigned.') + if bill_date: + path += '&bill_date=%s' % bill_date + else: + raise Exception('bill_date is not assigned.') + if account_type: + path += '&account_type=%s' % account_type + else: + raise Exception('account_type is not assigned.') + if algorithm: + path += '&algorithm=%s' % algorithm + else: + raise Exception('algorithm is not assigned.') + if tar_type: + path += '&tar_type=%s' % tar_type + return await self._core.request(path) + + +async def download_bill(self, url): + """下载账单 + :param url: 账单下载地址,示例值:'https://api.mch.weixin.qq.com/v3/billdownload/file?token=xxx' + """ + path = url[len(self._core._gate_way):] if url.startswith(self._core._gate_way) else url + return await self._core.request(path, skip_verify=True) + + +async def combine_pay(self, + combine_out_trade_no, + sub_orders, + scene_info=None, + combine_payer_info=None, + time_start=None, + time_expire=None, + combine_appid=None, + combine_mchid=None, + notify_url=None, + pay_type=None): + """合单支付下单 + :param combine_out_trade_no: 合单商户订单号, 示例值:'P20150806125346' + :param sub_orders: 子单信息,示例值:[{'mchid':'1900000109', 'attach':'深圳分店', 'amount':{'total_amount':100,'currency':'CNY'}, 'out_trade_no':'20150806125346', 'description':'腾讯充值中心-QQ会员充值', 'settle_info':{'profit_sharing':False, 'subsidy_amount':10}}] + :param scene_info: 场景信息, 示例值:{'device_id':'POS1:123', 'payer_client_ip':'14.17.22.32'} + :param combine_payer_info: 支付者, 示例值:{'openid':'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o'} + :param time_start: 交易起始时间,示例值:'2019-12-31T15:59:59+08:00' + :param time_expire: 交易结束时间, 示例值:'2019-12-31T15:59:59+08:00' + :param combine_appid: 合单商户appid, 示例值:'wxd678efh567hg6787' + :param combine_mchid: 合单发起方商户号,示例值:'1900000109' + :param notify_url: 通知地址, 示例值:'https://yourapp.com/notify' + :param pay_type: 微信支付类型,示例值:WeChatPayType.JSAPI + """ + params = {} + params.update({'combine_appid': combine_appid or self._appid}) + params.update({'combine_mchid': combine_mchid or self._mchid}) + if not (notify_url or self._notify_url): + raise Exception('notify_url is not assigned.') + params.update({'notify_url': notify_url or self._notify_url}) + if combine_out_trade_no: + params.update({'combine_out_trade_no': combine_out_trade_no}) + else: + raise Exception('combine_out_trade_no is not assigned.') + if sub_orders: + params.update({'sub_orders': sub_orders}) + else: + raise Exception('sub_orders is not assigned.') + if scene_info: + params.update({'scene_info': scene_info}) + if combine_payer_info: + params.update({'combine_payer_info': combine_payer_info}) + if time_start: + params.update({'time_start': time_start}) + if time_expire: + params.update({'time_expire': time_expire}) + pay_type = pay_type or self._type + if pay_type in [WeChatPayType.JSAPI, WeChatPayType.MINIPROG]: + if not combine_payer_info: + raise Exception('combine_payer_info is not assigned') + path = '/v3/combine-transactions/jsapi' + elif pay_type == WeChatPayType.APP: + path = '/v3/combine-transactions/app' + elif pay_type == WeChatPayType.H5: + if not scene_info: + raise Exception('scene_info is not assigned.') + path = '/v3/combine-transactions/h5' + elif pay_type == WeChatPayType.NATIVE: + path = '/v3/combine-transactions/native' + else: + raise Exception('pay_type is not assigned.') + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def combine_query(self, combine_out_trade_no): + """合单查询订单 + :param combine_out_trade_no: 合单商户订单号,示例值:P20150806125346 + """ + params = {} + if not combine_out_trade_no: + raise Exception('combine_out_trade_no is not assigned') + else: + params.update({'combine_out_trade_no': combine_out_trade_no}) + path = '/v3/combine-transactions/out-trade-no/%s' % combine_out_trade_no + return await self._core.request(path) + + +async def combine_close(self, combine_out_trade_no, sub_orders, combine_appid=None): + """合单关闭订单 + :param combine_out_trade_no: 合单商户订单号,示例值:'P20150806125346' + :param sub_orders: 子单信息, 示例值:[{'mchid': '1900000109', 'out_trade_no': '20150806125346'}] + :param combine_appid: 合单商户appid, 示例值:'wxd678efh567hg6787' + """ + params = {} + params.update({'combine_appid': combine_appid or self._appid}) + if not combine_out_trade_no: + raise Exception('combine_out_trade_no is not assigned.') + if not sub_orders: + raise Exception('sub_orders is not assigned.') + else: + params.update({'sub_orders': sub_orders}) + path = '/v3/combine-transactions/out-trade-no/%s/close' % combine_out_trade_no + return await self._core.request(path, method=RequestType.POST, data=params) + + +async def abnormal_refund(self, refund_id, out_refund_no, type, bank_type=None, bank_account=None, real_name=None, sub_mchid=None): + """发起异常退款 + :param refund_id: 微信退款单号,退款单的主键,唯一定义此资源的标识。 + :param out_refund_no: 商户退款单号,商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。 + :param type: 异常退款处理方式,可选值:'USER_BANK_CARD',退款到用户银行卡; 'MERCHANT_BANK_CARD',退款至交易商户银行账户。 + :param bank_type: 开户银行类型,采用字符串类型的银行标识,值列表详见官网银行类型。 + :param bank_account: 收款银行卡号,用户的银行卡账号。 + :param real_name: 收款用户姓名。 + """ + if refund_id: + path = '/v3/refund/domestic/refunds/%s/apply-abnormal-refund' % refund_id + else: + raise Exception('refund_id is not assigned.') + params = {} + if self._partner_mode: + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + if not (out_refund_no and type): + raise Exception('out_refund_no or type is not assigned.') + params.update({'out_refund_no': out_refund_no}) + params.update({'type': type}) + if bank_type: + params.update({'bank_type': bank_type}) + cipher_data = False + if bank_account: + params.update({'bank_account': self._core.encrypt(bank_account)}) + cipher_data = True + if real_name: + params.update({'real_name': self._core.encrypt(real_name)}) + cipher_data = True + return await self._core.request(path, method=RequestType.POST, data=params, cipher_data=cipher_data) + +async def codepay_reverse(self, out_trade_no, appid=None, mchid=None, sub_appid=None, sub_mchid=None): + """撤销付款码支付订单 + :警告:付款码支付订单如果用户已经付款,调用撤销接口会将资金退回给用户。: + :return code, message: + :param out_trade_no: 商户订单号,示例值:'1217752501201407033233368018' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param mchid: 微信支付商户号,可不填,默认传入初始化的mchid,示例值:'987654321' + :param sub_appid: (服务商模式)子商户应用ID,示例值:'wxd678efh567hg6999' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if self._partner_mode: + params.update({'sp_appid': appid or self._appid}) + params.update({'sp_mchid': mchid or self._mchid}) + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + if sub_appid: + params.update({'sub_appid': sub_appid}) + path = f'/v3/pay/partner/transactions/out-trade-no/{out_trade_no}/reverse' + else: + params.update({'appid': appid or self._appid}) + params.update({'mchid': mchid or self._mchid}) + path = f'/v3/pay/transactions/out-trade-no/{out_trade_no}/reverse' + return await self._core.request(path, method=RequestType.POST, data=params) diff --git a/wechatpayv3/async_/transfer.py b/wechatpayv3/async_/transfer.py new file mode 100644 index 0000000..d45d190 --- /dev/null +++ b/wechatpayv3/async_/transfer.py @@ -0,0 +1,216 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType + + +async def transfer_batch( + self, + out_batch_no, + batch_name, + batch_remark, + total_amount, + total_num, + transfer_detail_list=[], + appid=None, + transfer_scene_id=None, + notify_url=None, +): + """发起商家转账 + :param out_batch_no: 商户系统内部的商家批次单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一,示例值:'plfk2020042013' + :param batch_name: 该笔批量转账的名称,示例值:'2019年1月深圳分部报销单' + :param batch_remark: 转账说明,UTF8编码,最多允许32个字符,示例值:'2019年1月深圳分部报销单' + :param total_amount: 转账总金额,单位为分,必须与批次内所有明细转账金额之和保持一致,否则无法发起转账操作,示例值:'4000000' + :param total_num: 转账总笔数,必须与批次内所有明细之和保持一致,否则无法发起转账操作,示例值:200 + :param transfer_detail_list: 发起批量转账的明细列表,最多三千笔,示例值:[{"out_detail_no": "x23zy545Bd5436", "transfer_amount": 200000, "transfer_remark": "2020年4月报销", "openid": "o-MYE42l80oelYMDE34nYD456Xoy", "user_name": "张三"}] + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param transfer_scene_id: 转账场景ID,示例值:'1001' + :param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php' + """ + params = {} + if out_batch_no: + params.update({"out_batch_no": out_batch_no}) + else: + raise Exception("out_batch_no is not assigned") + if batch_name: + params.update({"batch_name": batch_name}) + else: + raise Exception("batch_name is not assigned") + if batch_remark: + params.update({"batch_remark": batch_remark}) + else: + raise Exception("batch_remark is not assigned") + if total_amount: + params.update({"total_amount": total_amount}) + else: + raise Exception("total_amount is not assigned") + if total_num: + params.update({"total_num": total_num}) + else: + raise Exception("total_num is not assigned") + if transfer_detail_list: + params.update({"transfer_detail_list": transfer_detail_list}) + else: + raise Exception("transfer_detail_list is not assigned") + cipher_data = False + for transfer_detail in params.get("transfer_detail_list"): + if transfer_detail.get("user_name"): + transfer_detail["user_name"] = self._core.encrypt( + transfer_detail.get("user_name") + ) + cipher_data = True + params.update({"appid": appid or self._appid}) + if notify_url or self._notify_url: + params.update({"notify_url": notify_url or self._notify_url}) + if transfer_scene_id: + params.update({"transfer_scene_id": transfer_scene_id}) + path = "/v3/transfer/batches" + return await self._core.request( + path, method=RequestType.POST, data=params, cipher_data=cipher_data + ) + + +async def transfer_query_batchid( + self, batch_id, need_query_detail=False, offset=0, limit=20, detail_status="ALL" +): + """微信批次单号查询批次单 + :param batch_id: 微信批次单号,微信商家转账系统返回的唯一标识,示例值:1030000071100999991182020050700019480001 + :param need_query_detail: 是否查询转账明细单,枚举值:true:是;false:否,默认否。 + :param offset: 请求资源起始位置,默认值为0 + :param limit: 最大资源条数,默认值为20 + :param detail_status: 明细状态, ALL:全部。需要同时查询转账成功和转账失败的明细单;SUCCESS:转账成功。只查询转账成功的明细单;FAIL:转账失败。 + """ + if batch_id: + path = "/v3/transfer/batches/batch-id/%s" % batch_id + else: + raise Exception("batch_id is not assigned") + if need_query_detail: + path += "?need_query_detail=true" + path += "&detail_status=%s" % detail_status + else: + path += "?need_query_detail=false" + path += "&offset=%s" % offset + path += "&limit=%s" % limit + return await self._core.request(path) + + +async def transfer_query_detail_id(self, batch_id, detail_id): + """微信明细单号查询明细单 + :param batch_id: 微信批次单号,微信商家转账系统返回的唯一标识,示例值:1030000071100999991182020050700019480001 + :param detail_id: 微信明细单号,微信支付系统内部区分转账批次单下不同转账明细单的唯一标识,示例值:1040000071100999991182020050700019500100 + """ + if batch_id and detail_id: + path = "/v3/transfer/batches/batch-id/%s/details/detail-id/%s" % ( + batch_id, + detail_id, + ) + else: + raise Exception("batch_id or detail_id is not assigned") + return await self._core.request(path) + + +async def transfer_query_out_batch_no( + self, out_batch_no, need_query_detail=False, offset=0, limit=20, detail_status="ALL" +): + """商家批次单号查询批次单 + :param out_batch_no: 商家批次单号,示例值:plfk2020042013 + :param need_query_detail: 是否查询转账明细单,枚举值:true:是;false:否,默认否。 + :param offset: 请求资源起始位置,默认值为0 + :param limit: 最大资源条数,默认值为20 + :param detail_status: 明细状态, ALL:全部。需要同时查询转账成功和转账失败的明细单;SUCCESS:转账成功。只查询转账成功的明细单;FAIL:转账失败。 + """ + if out_batch_no: + path = "/v3/transfer/batches/out-batch-no/%s" % out_batch_no + else: + raise Exception("batch_id is not assigned") + if need_query_detail: + path += "?need_query_detail=true" + path += "&detail_status=%s" % detail_status + else: + path += "?need_query_detail=false" + path += "&offset=%s" % offset + path += "&limit=%s" % limit + return await self._core.request(path) + + +async def transfer_query_out_detail_no(self, out_detail_no, out_batch_no): + """商家明细单号查询明细单 + :param out_detail_no: 商家明细单号,示例值:x23zy545Bd5436 + :param out_batch_no: 商家批次单号,示例值:plfk2020042013 + """ + if out_detail_no and out_batch_no: + path = "/v3/transfer/batches/out-batch-no/%s/details/out-detail-no/%s" % ( + out_batch_no, + out_detail_no, + ) + else: + raise Exception("out_detail_no or out_batch_no is not assigned") + return await self._core.request(path) + + +async def transfer_bill_receipt(self, out_batch_no): + """转账电子回单申请受理 + :param out_batch_no: 商家批次单号,示例值:plfk2020042013 + """ + params = {} + if out_batch_no: + params.update({"out_batch_no": out_batch_no}) + else: + raise Exception("out_batch_no is assigned") + path = "/v3/transfer/bill-receipt" + return await self._core.request(path, method=RequestType.POST, params=params) + + +async def transfer_query_bill_receipt(self, out_batch_no): + """查询转账电子回单 + :param out_batch_no: 商家批次单号,示例值:plfk2020042013 + """ + if out_batch_no: + path = "/v3/transfer/bill-receipt/%s" % out_batch_no + else: + raise Exception("out_batch_no is not assigned") + return await self._core.request(path) + + +async def transfer_detail_receipt( + self, + accept_type, + out_detail_no, + out_batch_no=None, +): + """转账明细电子回单受理 + :param accept_type: 受理类型 + :param out_detail_no: 商家明细单号,示例值:x23zy545Bd5436 + :param out_batch_no: 商家批次单号,示例值:plfk2020042013 + """ + params = {} + if accept_type: + params.update({"accept_type": accept_type}) + else: + raise Exception("accept_type is not assigned") + if out_detail_no: + params.update({"out_detail_no": out_detail_no}) + else: + raise Exception("out_detail_no is not assigned") + if out_batch_no: + params.update({"out_batch_no": out_batch_no}) + path = "/v3/transfer-detail/electronic-receipts" + return await self._core.request(path, method=RequestType.POST, params=params) + + +async def transfer_query_receipt(self, accept_type, out_detail_no, out_batch_no=None): + """查询转账明细电子回单受理结果 + :param accept_type: 受理类型 + :param out_detail_no: 商家明细单号,示例值:x23zy545Bd5436 + :param out_batch_no: 商家批次单号,示例值:plfk2020042013 + """ + if accept_type: + path = "/v3/transfer-detail/electronic-receipts?accept_type=%s" % accept_type + else: + raise Exception("accept_type is not assigned") + if out_detail_no: + path += "&out_batch_no=%s" % out_detail_no + else: + raise Exception("out_detail_no is not assigned") + if out_batch_no: + path += "&out_batch_no=%s" % out_batch_no + return await self._core.request(path) diff --git a/wechatpayv3/async_/type.py b/wechatpayv3/async_/type.py new file mode 100644 index 0000000..fc2e986 --- /dev/null +++ b/wechatpayv3/async_/type.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +from enum import Enum, unique + + +@unique +class RequestType(Enum): + GET = 'GET' + POST = 'POST' + PATCH = 'PATCH' + PUT = 'PUT' + DELETE = 'DELETE' + + +class WeChatPayType(Enum): + JSAPI = 0 + APP = 1 + H5 = 2 + NATIVE = 3 + MINIPROG = 4 + CODEPAY = 5 + + +class SignType(Enum): + RSA_SHA256 = 0 + HMAC_SHA256 = 1 + MD5 = 2 diff --git a/wechatpayv3/async_/utils.py b/wechatpayv3/async_/utils.py new file mode 100644 index 0000000..346882e --- /dev/null +++ b/wechatpayv3/async_/utils.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- + +import json +import time +import uuid +from base64 import b64decode, b64encode + +from cryptography.exceptions import InvalidSignature, InvalidTag +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP, PKCS1v15 +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.hazmat.primitives.hashes import SHA1, SHA256, SM3, Hash +from cryptography.hazmat.primitives.hmac import HMAC +from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key +from cryptography.x509 import load_pem_x509_certificate +from cryptography import __version__ as cryptography_version + + +def build_authorization(path, + method, + mchid, + serial_no, + private_key, + data=None, + nonce_str=None): + timeStamp = str(int(time.time())) + nonce_str = nonce_str or ''.join(str(uuid.uuid4()).split('-')).upper() + body = data if isinstance(data, str) else json.dumps(data, ensure_ascii=False, separators=(",", ":"), allow_nan=False) if data else '' + sign_str = '%s\n%s\n%s\n%s\n%s\n' % (method, path, timeStamp, nonce_str, body) + signature = rsa_sign(private_key=private_key, sign_str=sign_str) + authorization = 'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",signature="%s",timestamp="%s",serial_no="%s"' % (mchid, nonce_str, signature, timeStamp, serial_no) + return authorization + + +def rsa_sign(private_key, sign_str): + message = sign_str.encode('UTF-8') + signature = private_key.sign(data=message, padding=PKCS1v15(), algorithm=SHA256()) + sign = b64encode(signature).decode('UTF-8').replace('\n', '') + return sign + + +def aes_decrypt(nonce, ciphertext, associated_data, apiv3_key): + key_bytes = apiv3_key.encode('UTF-8') + nonce_bytes = nonce.encode('UTF-8') + associated_data_bytes = associated_data.encode('UTF-8') + data = b64decode(ciphertext) + aesgcm = AESGCM(key=key_bytes) + try: + result = aesgcm.decrypt(nonce=nonce_bytes, data=data, associated_data=associated_data_bytes).decode('UTF-8') + except InvalidTag: + result = None + return result + + +def format_private_key(private_key_str): + pem_start = '-----BEGIN PRIVATE KEY-----\n' + pem_end = '\n-----END PRIVATE KEY-----' + private_key_str = private_key_str.strip() + if not private_key_str.startswith(pem_start): + private_key_str = pem_start + private_key_str + if not private_key_str.endswith(pem_end): + private_key_str = private_key_str + pem_end + return private_key_str + + +def format_public_key(public_key_str): + pem_start = '-----BEGIN PUBLIC KEY-----\n' + pem_end = '\n-----END PUBLIC KEY-----' + public_key_str = public_key_str.strip() + if not public_key_str.startswith(pem_start): + public_key_str = pem_start + public_key_str + if not public_key_str.endswith(pem_end): + public_key_str = public_key_str + pem_end + return public_key_str + + +def load_certificate(certificate_str): + try: + return load_pem_x509_certificate(data=certificate_str.encode('UTF-8'), backend=default_backend()) + except: + return None + + +def load_private_key(private_key_str): + if not private_key_str: + return None + try: + return load_pem_private_key(data=format_private_key(private_key_str).encode('UTF-8'), password=None, backend=default_backend()) + except: + raise Exception('failed to load private key.') + + +def load_public_key(public_key_str): + if not public_key_str: + return None + try: + return load_pem_public_key(data=format_public_key(public_key_str).encode('UTF-8'), backend=default_backend()) + except: + raise Exception('failed to load public key.') + + +def rsa_verify(timestamp, nonce, body, signature, public_key): + sign_str = '%s\n%s\n%s\n' % (timestamp, nonce, body) + message = sign_str.encode('UTF-8') + try: + signature = b64decode(signature) + except: + return False + try: + public_key.verify(signature, message, PKCS1v15(), SHA256()) + except InvalidSignature: + return False + return True + + +def rsa_encrypt(text, public_key): + data = text.encode('UTF-8') + cipherbyte = public_key.encrypt( + plaintext=data, + padding=OAEP(mgf=MGF1(algorithm=SHA1()), algorithm=SHA1(), label=None) + ) + return b64encode(cipherbyte).decode('UTF-8') + + +def rsa_decrypt(ciphertext, private_key): + data = private_key.decrypt( + ciphertext=b64decode(ciphertext), + padding=OAEP(mgf=MGF1(algorithm=SHA1()), algorithm=SHA1(), label=None) + ) + result = data.decode('UTF-8') + return result + + +def hmac_sign(key, sign_str): + hmac = HMAC(key.encode('UTF-8'), SHA256()) + hmac.update(sign_str.encode('UTF-8')) + sign = hmac.finalize().hex().upper() + return sign + + +def sha256(data): + hash = Hash(SHA256()) + hash.update(data) + return hash.finalize().hex() + + +def sm3(data): + hash = Hash(SM3()) + hash.update(data) + return hash.finalize().hex() diff --git a/wechatpayv3/businesscircle.py b/wechatpayv3/businesscircle.py new file mode 100644 index 0000000..654bd3d --- /dev/null +++ b/wechatpayv3/businesscircle.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType + + +def points_notify(self, transaction_id, openid, earn_points, increased_points, points_update_time, no_points_remarks=None, total_points=None, appid=None, sub_mchid=None): + """智慧商圈积分同步 + :param transaction_id: 微信订单号,示例值:'1217752501201407033233368018' + :param openid: 用户标识,示例值:'oWmnN4xxxxxxxxxxe92NHIGf1xd8' + :param earn_points: 是否获得积分,示例值:True + :param increased_points: 订单新增积分值,示例值:100 + :param points_update_time: 积分更新时间,示例值:'2020-05-20T13:29:35.120+08:00' + :param no_points_remarks: 未获得积分的备注信息,示例值:'商品不参与积分活动' + :param total_points: 顾客积分总额,示例值:888888 + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + params.update({'appid': appid or self._appid}) + if transaction_id: + params.update({'transaction_id': transaction_id}) + else: + raise Exception('transaction_id is not assigned.') + if openid: + params.update({'openid': openid}) + else: + raise Exception('openid is not assigned.') + if earn_points: + params.update({'earn_points': earn_points}) + else: + raise Exception('earn_points is not assigned.') + if increased_points: + params.update({'increased_points': increased_points}) + else: + raise Exception('increased_points is not assigned') + if points_update_time: + params.update({'points_update_time': points_update_time}) + else: + raise Exception('points_update_time is not assigned.') + if no_points_remarks: + params.update({'no_points_remarks': no_points_remarks}) + if total_points: + params.update({'total_points': total_points}) + if self._partner_mode and sub_mchid: + params.update({'sub_mchid': sub_mchid}) + path = '/v3/businesscircle/points/notify' + return self._core.request(path, method=RequestType.POST, data=params) + + +def user_authorization(self, openid, appid=None, sub_mchid=None): + """智慧商圈积分授权查询 + :param openid: 用户标识,示例值:'oWmnN4xxxxxxxxxxe92NHIGf1xd8' + :param appid: 小程序appid,顾客授权积分时使用的小程序的appid,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + if openid: + if self._partner_mode: + path = '/v3/businesscircle/user-authorizations/%s?appid=%s' % (openid, appid or self._appid) + if sub_mchid: + path = '%s&sub_mchid=%s' % (path, sub_mchid) + else: + path = '/v3/businesscircle/user-authorizations/%s?appid=%s' % (openid, self._appid) + else: + raise Exception('openid is not assigned.') + return self._core.request(path) + + +def business_parking_sync(self, openid, brandid, plate_number, state, time, appid=None, sub_mchid=None): + """商圈会员停车状态同步 + :param openid: 用户标识,示例值:'oWmnN4xxxxxxxxxxe92NHIGf1xd8' + :param brandid: 品牌ID,示例值:1000 + :param plate_number: 车牌号,示例值: '粤B888888' + :param state: 停车状态,IN=入场,用户开车进入商圈,OUT=离场,用户开车离开商圈。示例值:IN + :param time: 时间,示例值:2022-06-01T10:43:39+08:00 + :param appid: 小程序appid,顾客授权积分时使用的小程序的appid,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + params.update({'appid': appid or self._appid}) + if not openid: + raise Exception('openid is not assigned.') + else: + params.update({'openid': openid}) + if not brandid: + raise Exception('brandid is not assigned.') + else: + params.update({'brandid': brandid}) + if not plate_number: + raise Exception('plate_number is not assigned.') + else: + params.update({'plate_number': plate_number}) + if not state: + raise Exception('state is not assigned.') + else: + params.update({'state': state}) + if not time: + raise Exception('time is not assigned.') + else: + params.update({'time': time}) + if self._partner_mode: + if not sub_mchid: + raise Exception('sub_mchid is not assigned.') + else: + params.update({'sub_mchid': sub_mchid}) + path = 'https://api.mch.weixin.qq.com/v3/businesscircle/parkings' + return self._core.request(path, method=RequestType.POST, date=params) + + +def business_point_status(self, openid, brandid, appid=None, sub_mchid=None): + """商圈会员待积分状态查询 + :param openid: 用户标识,示例值:'oWmnN4xxxxxxxxxxe92NHIGf1xd8' + :param brandid: 品牌ID,示例值:1000 + :param appid: 小程序appid,顾客授权积分时使用的小程序的appid,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + if not (openid and brandid): + raise Exception('openid and/or brandid is not assigned.') + else: + path = 'https://api.mch.weixin.qq.com/v3/businesscircle/users/%s/points/commit_status?brandid=%s&appid=%s' % (openid, brandid, appid or self._appid) + if sub_mchid: + path += '%s&sub_mchid=%s' % (path, sub_mchid) + return self._core.request(path) diff --git a/wechatpayv3/capital.py b/wechatpayv3/capital.py new file mode 100644 index 0000000..79c7fb5 --- /dev/null +++ b/wechatpayv3/capital.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + + +def capital_search_bank_number(self, account_number): + """获取对私银行卡号开户银行 + :param account_number: 银行卡号,示例值:'1234567890123' + """ + from urllib.parse import urlencode + params = {} + params.update({'account_number': self._core.encrypt(account_number)}) + path = '/v3/capital/capitallhh/banks/search-banks-by-bank-account?%s' % urlencode(params) + return self._core.request(path, cipher_data=True) + + +def capital_personal_banks(self, offset=0, limit=200): + """查询支持个人业务的银行列表 + :param offset: 本次查询偏移量,示例值:0 + :param offset: 本次请求最大查询条数,示例值:200 + """ + path = '/v3/capital/capitallhh/banks/personal-banking?offset=%s&limit=%s' % (offset, limit) + return self._core.request(path) + + +def capital_corporate_banks(self, offset=0, limit=200): + """查询支持对公业务的银行列表 + :param offset: 本次查询偏移量,示例值:0 + :param offset: 本次请求最大查询条数,示例值:200 + """ + path = '/v3/capital/capitallhh/banks/corporate-banking?offset=%s&limit=%s' % (offset, limit) + return self._core.request(path) + + +def capital_provinces(self): + """查询省份列表 + """ + path = '/v3/capital/capitallhh/areas/provinces' + return self._core.request(path) + + +def capital_cities(self, province_code): + """查询城市列表 + :param province_code: 省份编码,唯一标识一个省份。示例值:10 + """ + path = '/v3/capital/capitallhh/areas/provinces/%s/cities' % province_code + return self._core.request(path) + + +def capital_branches(self, bank_alias_code, city_code, offset=0, limit=100): + """查询支行列表 + :param bank_alias_code: 银行别名的编码,查询支行接口仅支持需要填写支行的银行别名编码。示例值:1000006247 + :param city_code: 城市编码,唯一标识一座城市,用于结合银行别名编码查询支行列表。示例值:536 + :param offset: 本次查询偏移量,示例值:0 + :param offset: 本次请求最大查询条数,示例值:100 + """ + if bank_alias_code and city_code: + path = '/v3/capital/capitallhh/banks/%s/branches?city_code=%s&offset=%s&limit=%s' % (bank_alias_code, city_code, offset, limit) + else: + raise Exception('bank_alias_code or city_code is not assigned.') + return self._core.request(path) diff --git a/wechatpayv3/complaint.py b/wechatpayv3/complaint.py new file mode 100644 index 0000000..3dd333d --- /dev/null +++ b/wechatpayv3/complaint.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- + +from datetime import datetime + +from .media import _media_upload +from .type import RequestType + + +def complaint_list_query(self, begin_date=None, end_date=None, limit=10, offset=0, complainted_mchid=None): + """查询投诉单列表 + :param begin_date: 开始日期,投诉发生的开始日期,格式为YYYY-MM-DD。注意,查询日期跨度不超过30天,当前查询为实时查询。示例值:'2019-01-01' + :param end_date: 结束日期,投诉发生的结束日期,格式为YYYY-MM-DD。注意,查询日期跨度不超过30天,当前查询为实时查询。示例值:'2019-01-01' + :param limit: 分页大小,设置该次请求返回的最大投诉条数,范围【1,50】,商户自定义字段,不传默认为10。示例值:5 + :param offset: 分页开始位置,该次请求的分页开始位置,从0开始计数,例如offset=10,表示从第11条记录开始返回,不传默认为0 。示例值:10 + :param complainted_mchid: 被诉商户号,投诉单对应的被诉商户号。示例值:'1900012181' + """ + if not begin_date: + begin_date = datetime.now().strftime("%Y-%m-%d") + if not end_date: + end_date = begin_date + if not complainted_mchid: + complainted_mchid = self._mchid + path = '/v3/merchant-service/complaints-v2?limit=%s&offset=%s&begin_date=%s&end_date=%s&complainted_mchid=%s' + path = path % (limit, offset, begin_date, end_date, complainted_mchid) + return self._core.request(path) + + +def complaint_detail_query(self, complaint_id): + """查询投诉单详情 + :param complaint_id: 投诉单对应的投诉单号。示例值:'200201820200101080076610000' + """ + if not complaint_id: + raise Exception('complaint_id is not assigned.') + path = '/v3/merchant-service/complaints-v2/%s' % complaint_id + return self._core.request(path) + + +def complaint_history_query(self, complaint_id, limit=100, offset=0): + """查询投诉协商历史 + :param complaint_id: 投诉单对应的投诉单号。示例值:'200201820200101080076610000' + :param limit: 分页大小,设置该次请求返回的最大协商历史条数,范围[1,300],不传默认为100。。示例值:5 + :param offset: 分页开始位置,该次请求的分页开始位置,从0开始计数,例如offset=10,表示从第11条记录开始返回,不传默认为0。示例值:10 + """ + if not complaint_id: + raise Exception('complaint_id is not assigned.') + if limit not in range(1, 301): + limit = 100 + path = '/v3/merchant-service/complaints-v2/%s/negotiation-historys?limit=%s&offset=%s' % (complaint_id, limit, offset) + return self._core.request(path) + + +def complaint_notification_create(self, url): + """创建投诉通知回调地址 + :param: url: 通知地址,仅支持https。示例值:'https://www.xxx.com/notify' + """ + params = {} + if url: + params.update({'url': url}) + else: + raise Exception('url is not assigned.') + path = '/v3/merchant-service/complaint-notifications' + return self._core.request(path, method=RequestType.POST, data=params) + + +def complaint_notification_query(self): + """查询投诉通知回调地址 + :param: url: 通知地址,仅支持https。示例值:'https://www.xxx.com/notify' + """ + path = '/v3/merchant-service/complaint-notifications' + return self._core.request(path) + + +def complaint_notification_update(self, url): + """更新投诉通知回调地址 + :param: url: 通知地址,仅支持https。示例值:'https://www.xxx.com/notify' + """ + params = {} + if url: + params.update({'url': url}) + else: + raise Exception('url is not assigned.') + path = '/v3/merchant-service/complaint-notifications' + return self._core.request(path, method=RequestType.PUT, data=params) + + +def complaint_notification_delete(self): + """删除投诉通知回调地址 + :param: url: 通知地址,仅支持https。示例值:'https://www.xxx.com/notify' + """ + path = '/v3/merchant-service/complaint-notifications' + return self._core.request(path, method=RequestType.DELETE) + + +def complaint_response(self, complaint_id, response_content, response_images=None, jump_url=None, jump_url_text=None, mini_program_jump_info=None): + """提交投诉回复 + :param complaint_id: 投诉单对应的投诉单号。示例值:'200201820200101080076610000' + :param response_content: 回复内容,具体的投诉处理方案,限制200个字符以内。示例值:'已与用户沟通解决' + :param response_images: 回复图片,传入调用商户上传反馈图片接口返回的media_id,最多上传4张图片凭证。示例值:['file23578_21798531.jpg', 'file23578_21798532.jpg'] + :param jump_url: 跳转链接,附加跳转链接,引导用户跳转至商户客诉处理页面,链接需满足https格式。示例值:"https://www.xxx.com/notify" + :param jump_url_text: 转链接文案,展示给用户的文案,附在回复内容之后。用户点击文案,即可进行跳转。示例值:"查看订单详情" + :mini_program_jump_info: 跳转小程序信息,商户可在回复中附加小程序信息,引导用户跳转至商户客诉处理小程序。示例值:{"appid" : "example_appid","path" : "example_path","text" : "example_text"} + """ + params = {} + if not complaint_id: + raise Exception('complaint_id is not assigned') + if response_content: + params.update({'response_content': response_content}) + else: + raise Exception('response_content is not assigned') + params.update({'complainted_mchid': self._core._mchid}) + if response_images: + params.update({'response_images': response_images}) + if jump_url: + params.update({'jump_url': jump_url}) + if jump_url_text: + params.update({'jump_url_text': jump_url_text}) + if mini_program_jump_info: + params.update({'mini_program_jump_info': mini_program_jump_info}) + path = '/v3/merchant-service/complaints-v2/%s/response' % complaint_id + return self._core.request(path, method=RequestType.POST, data=params) + + +def complaint_complete(self, complaint_id): + """反馈投诉处理完成 + :param complaint_id: 投诉单对应的投诉单号。示例值:'200201820200101080076610000' + """ + params = {} + if not complaint_id: + raise Exception('complaint_id is not assigned') + params.update({'complainted_mchid': self._core._mchid}) + path = '/v3/merchant-service/complaints-v2/%s/complete' % complaint_id + return self._core.request(path, method=RequestType.POST, data=params) + + +def complaint_image_upload(self, filepath, filename=None): + """商户上传投诉反馈图片 + :param filepath: 图片文件路径 + :param filename: 文件名称,未指定则从filepath参数中截取 + """ + return _media_upload(self, filepath, filename, '/v3/merchant-service/images/upload') + + +def complaint_image_download(self, media_url): + """下载客户投诉图片 + :param media_url: 图片下载地址,示例值:'https://api.mch.weixin.qq.com/v3/merchant-service/images/xxxxx' + """ + path = media_url[len(self._core._gate_way):] if media_url.startswith(self._core._gate_way) else media_url + return self._core.request(path, skip_verify=True) + + +def complaint_update_refund(self, complaint_id, action, launch_refund_day=None, reject_reason=None, reject_media_list={}, remark=None): + """更新退款审批结果 + :param compaint_id: 投诉单对应的投诉单号。示例值:'200201820200101080076610000' + :param action: 审批动作,同意 或 拒绝,REJECT:拒绝,拒绝退款;APPROVE:同意,同意退款;示例值:'APPROVE' + :param launch_refund_day: 预计发起退款时间,预计将在多少个工作日内能发起退款, 0代表当天。示例值:3 + :param reject_reason: 拒绝退款原因,示例值:'拒绝退款' + :param reject_media_list: 拒绝退款的举证图片列表,传入调用“商户上传反馈图片”接口返回的media_id,最多上传4张图片凭证,示例值:{'file23578_21798531.jpg'} + :param remark: 备注,示例值:'已处理完成' + """ + if complaint_id: + path = '/v3/merchant-service/complaints-v2/%s/update-refund-progress' % complaint_id + else: + raise Exception('complaint_id is not assigned') + params = {} + if action: + params.update({'action': action}) + else: + raise Exception('action is not assigned') + if isinstance(launch_refund_day, int): + params.update({'launch_refund_day': launch_refund_day}) + if reject_reason: + params.update({'reject_reason': reject_reason}) + if reject_media_list: + params.update({'reject_media_list': reject_media_list}) + if remark: + params.update({'remark': remark}) + return self._core.request(path, method=RequestType.POST, data=params) diff --git a/wechatpayv3/core.py b/wechatpayv3/core.py new file mode 100644 index 0000000..a63d5e5 --- /dev/null +++ b/wechatpayv3/core.py @@ -0,0 +1,278 @@ +# -*- coding: utf-8 -*- + +import json +import os +from datetime import datetime, timezone + +import requests + +from .type import RequestType, SignType +from .utils import (aes_decrypt, build_authorization, hmac_sign, load_public_key, + load_certificate, load_private_key, rsa_decrypt, + rsa_encrypt, rsa_sign, rsa_verify, cryptography_version) + + +class Core(): + def __init__(self, mchid, cert_serial_no, private_key, apiv3_key, cert_dir=None, logger=None, proxy=None, timeout=None, public_key=None, public_key_id=None): + self._proxy = proxy + self._mchid = mchid + self._cert_serial_no = cert_serial_no + self._private_key = load_private_key(private_key) + self._apiv3_key = apiv3_key + self._gate_way = 'https://api.mch.weixin.qq.com' + self._certificates = [] + self._cert_dir = cert_dir + '/' if cert_dir else None + self._logger = logger + self._timeout = timeout + self._public_key = load_public_key(public_key) + self._public_key_id = public_key_id + if (public_key is None) != (public_key_id is None): + raise Exception('public_key_id or public_key is not assigned.') + if not self._public_key: + self._init_certificates() + + def _update_certificates(self): + path = '/v3/certificates' + self._certificates.clear() + code, message = self.request(path, skip_verify=True) + if code != 200: + return + data = json.loads(message).get('data') + for value in data: + serial_no = value.get('serial_no') + effective_time = value.get('effective_time') + expire_time = value.get('expire_time') + encrypt_certificate = value.get('encrypt_certificate') + algorithm = nonce = associated_data = ciphertext = None + if encrypt_certificate: + algorithm = encrypt_certificate.get('algorithm') + nonce = encrypt_certificate.get('nonce') + associated_data = encrypt_certificate.get('associated_data') + ciphertext = encrypt_certificate.get('ciphertext') + if not (serial_no and effective_time and expire_time and algorithm and nonce and associated_data and ciphertext): + continue + cert_str = aes_decrypt( + nonce=nonce, + ciphertext=ciphertext, + associated_data=associated_data, + apiv3_key=self._apiv3_key) + certificate = load_certificate(cert_str) + if not certificate: + continue + if (int(cryptography_version.split(".")[0]) < 42): + now = datetime.utcnow() + if now < certificate.not_valid_before or now > certificate.not_valid_after: + continue + else: + now = datetime.now(timezone.utc) + if now < certificate.not_valid_before_utc or now > certificate.not_valid_after_utc: + continue + self._certificates.append(certificate) + if not self._cert_dir: + continue + if not os.path.exists(self._cert_dir): + os.makedirs(self._cert_dir) + if not os.path.exists(self._cert_dir + serial_no + '.pem'): + with open(self._cert_dir + serial_no + '.pem', 'w') as f: + f.write(cert_str) + + def _verify_signature(self, headers, body): + signature_mark = 'Wechatpay-Signature' + timestamp_mark = 'Wechatpay-Timestamp' + nonce_mark = 'Wechatpay-Nonce' + serial_mark = 'Wechatpay-Serial' + signature_type_mark = 'Wechatpay-Signature-Type' + if headers.get('HTTP_WECHATPAY_SIGNATURE'): # 兼容django + signature_mark = 'HTTP_WECHATPAY_SIGNATURE' + timestamp_mark = 'HTTP_WECHATPAY_TIMESTAMP' + nonce_mark = 'HTTP_WECHATPAY_NONCE' + serial_mark = 'HTTP_WECHATPAY_SERIAL' + signature_type_mark = 'HTTP_WECHATPAY_SIGNATURE_TYPE' + if headers.get('wechatpay-signature'): # 兼容fastapi + signature_mark = 'wechatpay-signature' + timestamp_mark = 'wechatpay-timestamp' + nonce_mark = 'wechatpay-nonce' + serial_mark = 'wechatpay-serial' + signature_type_mark = 'wechatpay-signature-type' + signature = headers.get(signature_mark, '') + timestamp = headers.get(timestamp_mark, '') + nonce = headers.get(nonce_mark, '') + serial_no = headers.get(serial_mark, '') + signature_type = headers.get(signature_type_mark, '') + if signature_type != 'WECHATPAY2-SHA256-RSA2048': + raise Exception(f'wechatpayv3 does not support this algorithm: {signature_type}') + if serial_no == self._public_key_id: + public_key = self._public_key + elif serial_no.startswith('PUB_KEY_ID_'): + # 微信支付新格式:不匹配传统十六进制证书序列号,尝试用所有已加载的证书验证 + for cert in self._certificates: + if rsa_verify(timestamp, nonce, body, signature, cert.public_key()): + return True + return False + else: + cert_found = False + for cert in self._certificates: + if int('0x' + serial_no, 16) == cert.serial_number: + cert_found = True + certificate = cert + break + if not cert_found: + self._update_certificates() + for cert in self._certificates: + if int('0x' + serial_no, 16) == cert.serial_number: + cert_found = True + certificate = cert + break + if not cert_found: + return False + public_key = certificate.public_key() + if not rsa_verify(timestamp, nonce, body, signature, public_key): + return False + return True + + def request(self, path, method=RequestType.GET, data=None, skip_verify=False, sign_data=None, files=None, cipher_data=False, headers={}): + if files: + headers.update({'Content-Type': 'multipart/form-data'}) + else: + headers.update({'Content-Type': 'application/json'}) + headers.update({'Accept': 'application/json'}) + headers.update({'User-Agent': 'wechatpay python sdk v1.3.11(https://github.com/minibear2021/wechatpayv3)'}) + if self._public_key_id or cipher_data: + wechatpay_serial = self._public_key_id if self._public_key_id else hex(self._last_certificate().serial_number)[2:].upper() + headers.update({'Wechatpay-Serial': wechatpay_serial}) + authorization = build_authorization( + path, + method.value, + self._mchid, + self._cert_serial_no, + self._private_key, + data=sign_data if sign_data else data) + headers.update({'Authorization': authorization}) + if self._logger: + self._logger.debug('Request url: %s' % self._gate_way + path) + self._logger.debug('Request type: %s' % method.value) + self._logger.debug('Request headers: %s' % headers) + self._logger.debug('Request params: %s' % data) + if method == RequestType.GET: + response = requests.get(url=self._gate_way + path, headers=headers, proxies=self._proxy, timeout=self._timeout) + elif method == RequestType.POST: + response = requests.post(url=self._gate_way + path, json=None if files else data, data=data if files else None, headers=headers, files=files, proxies=self._proxy, timeout=self._timeout) + elif method == RequestType.PATCH: + response = requests.patch(url=self._gate_way + path, json=data, headers=headers, proxies=self._proxy, timeout=self._timeout) + elif method == RequestType.PUT: + response = requests.put(url=self._gate_way + path, json=data, headers=headers, proxies=self._proxy, timeout=self._timeout) + elif method == RequestType.DELETE: + response = requests.delete(url=self._gate_way + path, headers=headers, proxies=self._proxy, timeout=self._timeout) + else: + raise Exception('wechatpayv3 does no support this request type.') + if self._logger: + self._logger.debug('Response status code: %s' % response.status_code) + self._logger.debug('Response headers: %s' % response.headers) + self._logger.debug('Response content: %s' % response.text) + if response.status_code in range(200, 300) and not skip_verify: + if not self._verify_signature(response.headers, response.text): + raise Exception('failed to verify the signature') + return response.status_code, response.text if 'application/json' in response.headers.get('Content-Type', '') else response.content + + def sign(self, data, sign_type=SignType.RSA_SHA256): + if sign_type == SignType.RSA_SHA256: + sign_str = '\n'.join(data) + '\n' + return rsa_sign(self._private_key, sign_str) + elif sign_type == SignType.HMAC_SHA256: + key_list = sorted(data.keys()) + sign_str = '' + for k in key_list: + v = data[k] + sign_str += str(k) + '=' + str(v) + '&' + sign_str += 'key=' + self._apiv3_key + return hmac_sign(self._apiv3_key, sign_str) + else: + raise ValueError('unexpected value of sign_type.') + + def decrypt_callback(self, headers, body): + if isinstance(body, bytes): + body = body.decode('UTF-8') + if self._logger: + self._logger.debug('Callback headers: %s' % headers) + self._logger.debug('Callback body: %s' % body) + if not self._verify_signature(headers, body): + if self._logger: + self._logger.debug('Failed to verify signature') + return None + data = json.loads(body) + resource_type = data.get('resource_type') + if resource_type != 'encrypt-resource': + return None + resource = data.get('resource') + if not resource: + return None + algorithm = resource.get('algorithm') + if algorithm != 'AEAD_AES_256_GCM': + raise Exception(f'wechatpayv3 does not support this algorithm: {algorithm}') + nonce = resource.get('nonce') + ciphertext = resource.get('ciphertext') + associated_data = resource.get('associated_data') + if not (nonce and ciphertext): + return None + if not associated_data: + associated_data = '' + result = aes_decrypt( + nonce=nonce, + ciphertext=ciphertext, + associated_data=associated_data, + apiv3_key=self._apiv3_key) + if self._logger: + self._logger.debug('Callback result: %s' % result) + if not result: + self._logger.debug('Please double check your apiv3 key') + return result + + def callback(self, headers, body): + if isinstance(body, bytes): + body = body.decode('UTF-8') + result = self.decrypt_callback(headers=headers, body=body) + if result: + data = json.loads(body) + data.update({'resource': json.loads(result)}) + return data + else: + return result + + def _init_certificates(self): + if self._cert_dir and os.path.exists(self._cert_dir): + for file_name in os.listdir(self._cert_dir): + if not file_name.lower().endswith('.pem'): + continue + with open(self._cert_dir + file_name, encoding="utf-8") as f: + certificate = load_certificate(f.read()) + if (int(cryptography_version.split(".")[0]) < 42): + now = datetime.utcnow() + if certificate and now >= certificate.not_valid_before and now <= certificate.not_valid_after: + self._certificates.append(certificate) + else: + now = datetime.now(timezone.utc) + if certificate and now >= certificate.not_valid_before_utc and now <= certificate.not_valid_after_utc: + self._certificates.append(certificate) + if not self._certificates: + self._update_certificates() + if not self._certificates: + raise Exception('No wechatpay platform certificate, please double check your init params.') + + def decrypt(self, ciphtext): + return rsa_decrypt(ciphertext=ciphtext, private_key=self._private_key) + + def encrypt(self, text): + if self._public_key_id: + public_key = self._public_key + else: + public_key = self._last_certificate().public_key() + return rsa_encrypt(text=text, public_key=public_key) + + def _last_certificate(self): + if not self._certificates: + self._update_certificates() + certificate = self._certificates[0] + for cert in self._certificates: + if certificate.not_valid_after < cert.not_valid_after: + certificate = cert + return certificate diff --git a/wechatpayv3/fapiao.py b/wechatpayv3/fapiao.py new file mode 100644 index 0000000..f8f3883 --- /dev/null +++ b/wechatpayv3/fapiao.py @@ -0,0 +1,279 @@ +# -*- coding: utf-8 -*- +import os.path + +from .type import RequestType +from .utils import sm3 + +# https://pay.weixin.qq.com/wiki/doc/apiv3/Offline/open/chapter4_8_1.shtml + + +def fapiao_card_template(self, card_template_information, card_appid=None): + """创建电子发票卡券模板 + :param card_template_information: 卡券模板信息。示例值:{'logo_url':'http://mmbiz.qpic.cn/mmbiz/iaL1LJM1mF9aRKPZJkmG8xX'} + :param card_appid: 插卡公众号AppID,若是服务商模式,则可以是服务商申请的appid,也可以是子商户申请的appid;若是直连模式,则是直连商户申请的appid。示例值:wxb1170446a4c0a5a2 + """ + if not card_appid: + card_appid = self._appid + params = {} + params.update({'card_appid': card_appid}) + if card_template_information: + params.update({'card_template_information': card_template_information}) + else: + raise Exception('card_template_information is not assigned.') + path = '/v3/new-tax-control-fapiao/card-template' + return self._core.request(path, method=RequestType.POST, data=params) + + +def fapiao_set_merchant_config(self, callback_url=None): + """配置开发选项 + :param callback_url: 商户回调地址。收取微信的授权通知、开票通知、插卡通知等相关通知。示例值:'https://pay.weixin.qq.com/callback' + """ + if not callback_url: + callback_url = self._notify_url + params = {} + params.update({'callback_url': callback_url}) + path = '/v3/new-tax-control-fapiao/merchant/development-config' + return self._core.request(path, method=RequestType.PATCH, data=params) + + +def fapiao_merchant_config(self): + """查询商户配置的开发选项 + """ + path = '/v3/new-tax-control-fapiao/merchant/development-config' + return self._core.request(path) + + +def fapiao_title_url(self, fapiao_apply_id, source, total_amount, openid, appid=None, + seller_name=None, show_phone_cell=False, must_input_phone=False, + show_email_cell=False, must_input_email=False): + """获取抬头填写链接 + :param fapiao_apply_id: 发票申请单号,示例值:'4200000444201910177461284488' + :param source: 开票来源,WEB:微信H5开票,MINIPROGRAM:微信小程序开票,示例值:'WEB' + :param total_amount: 总金额,单位:分,示例值:100 + :param openid: 需要填写发票抬头的用户在商户AppID下的OpenID,示例值:'plN5twRbHym_j-QcqCzstl0HmwEs' + :param appid: 若开票来源是WEB,则为商户的公众号AppID;若开票来源是MINIPROGRAM,则为商户的小程序AppID,示例值:'wxb1170446a4c0a5a2' + :param seller_name: 销售方名称,若不传则默认取商户名称,示例值:'深圳市南山区测试商户' + :param show_phone_cell: 是否需要展示手机号填写栏 + :param must_input_phone: 是否必须填写手机号,仅当需要展示手机号填写栏时生效 + :param show_email_cell: 是否需要展示邮箱地址填写栏 + :param must_input_email: 是否必须填写邮箱地址,仅当需要展示邮箱地址填写栏时生效 + """ + path = '/v3/new-tax-control-fapiao/user-title/title-url?' + if fapiao_apply_id: + path += 'fapiao_apply_id=%s' % fapiao_apply_id + else: + raise Exception('fapiao_apply_id is not assigned.') + if source: + path += '&source=%s' % source + else: + raise Exception('source is not assigned.') + if total_amount: + path += '&total_amount=%s' % total_amount + else: + raise Exception('total_amount is not assigned.') + if appid: + path += '&appid=%s' % appid + else: + path += '&appid=%s' % self._appid + if openid: + path += '&openid=%s' % openid + else: + raise Exception('openid is not assigned.') + if seller_name: + path += '&seller_name=%s' % seller_name + if show_phone_cell: + path += '&show_phone_cell=true' + if must_input_phone: + path += '&must_input_phone=true' + if show_email_cell: + path += '&show_email_cell=true' + if must_input_email: + path += '&must_input_email=true' + return self._core.request(path) + + +def fapiao_title(self, fapiao_apply_id, scene='WITH_WECHATPAY'): + """获取用户填写的抬头 + :param fapiao_apply_id: 发票申请单号,示例值:'4200000444201910177461284488' + :param scene: 场景值,目前只支持WITH_WECHATPAY。示例值:'WITH_WECHATPAY' + """ + path = '/v3/new-tax-control-fapiao/user-title?' + if fapiao_apply_id: + path += 'fapiao_apply_id=%s' % fapiao_apply_id + else: + raise Exception('fapiao_apply_id is not assigned.') + path += '&scene=%s' % scene + return self._core.request(path) + + +def fapiao_tax_codes(self, offset=0, limit=20): + """获取商品和服务税收分类对照表 + :param offset: 查询的起始位置,示例值:0 + :param limit: 查询的最大数量,最大值20 + """ + path = '/v3/new-tax-control-fapiao/merchant/tax-codes?offset=%s&limit=%s' % (offset, limit) + return self._core.request(path) + + +def fapiao_merchant_base_info(self): + """获取商户开票基础信息 + """ + path = '/v3/new-tax-control-fapiao/merchant/base-information' + return self._core.request(path) + + +def fapiao_applications(self, fapiao_apply_id, buyer_information, fapiao_information, scene='WITH_WECHATPAY'): + """开具电子发票 + :param fapiao_apply_id: 发票申请单号,示例值:'4200000444201910177461284488' + :param buyer_information: 购买方信息,示例值:{'type':'ORGANIZATION','name':'深圳市南山区测试企业'} + :param fapiao_information: 需要开具的发票信息,示例值:[{'fapiao_id':'20200701123456','total_amount':382895,'need_list':False,'items':[{'tax_code':'3010101020203000000','quantity':100000000,'total_amount':'429900','discount':False}]}] + :param scene: 场景值,目前只支持WITH_WECHATPAY。示例值:'WITH_WECHATPAY' + """ + params = {} + if fapiao_apply_id: + params.update({'fapiao_apply_id': fapiao_apply_id}) + else: + raise Exception('fapiao_aply_id is not assigned.') + cipher_data = False + if buyer_information: + if buyer_information.get('phone'): + buyer_information.update({'phone': self._core.encrypt(buyer_information.get('phone'))}) + cipher_data = True + if buyer_information.get('email'): + buyer_information.update({'email': self._core.encrypt(buyer_information.get('email'))}) + cipher_data = True + params.update({'buyer_information': buyer_information}) + else: + raise Exception('buyer_information is not assigned.') + if fapiao_information: + params.update({'fapiao_information': fapiao_information}) + else: + raise Exception('fapiao_information is not assigned.') + params.update({'scene': scene}) + path = '/v3/new-tax-control-fapiao/fapiao-applications' + return self._core.request(path, method=RequestType.POST, data=params, cipher_data=cipher_data) + + +def fapiao_query(self, fapiao_apply_id, fapiao_id=None): + """查询电子发票 + :param fapiao_apply_id: 发票申请单号,示例值:'4200000444201910177461284488' + :param fapiao_id: 商户发票单号,示例值:'20200701123456' + """ + path = '/v3/new-tax-control-fapiao/fapiao-applications/%s' % fapiao_apply_id + if fapiao_id: + path += '?fapiao_id=%s' % fapiao_id + return self._core.request(path) + + +def fapiao_reverse(self, fapiao_apply_id, reverse_reason, fapiao_information): + """冲红电子发票 + :param fapiao_apply_id: 发票申请单号,示例值:'4200000444201910177461284488' + :param reverse_reason: 冲红原因,示例值:'退款' + :param fapiao_information: 需要冲红的发票信息,示例值:{'fapiao_id':'20200701123456','fapiao_code':'044001911211','fapiao_number':'12897794'} + """ + if fapiao_apply_id: + path = '/v3/new-tax-control-fapiao/fapiao-applications/%s/reverse' % fapiao_apply_id + else: + raise Exception('fapiao_apply_id is not assigned.') + params = {} + if reverse_reason: + params.update({'reverse_reason': reverse_reason}) + else: + raise Exception('reverse_reason is not assigned.') + if fapiao_information: + params.update({'fapiao_information': fapiao_information}) + else: + raise Exception('fapiao_information is not assigned.') + return self._core.request(path, method=RequestType.POST, data=params) + + +def fapiao_upload_file(self, filepath): + """上传电子发票文件 + :filepath: 电子发票文件路径,只支持pdf和odf两种格式,示例值:'./fapiao/0001.pdf' + """ + if not (filepath and os.path.exists(filepath) and os.path.isfile(filepath)): + raise Exception('filepath is not assigned or not exists') + with open(filepath, mode='rb') as f: + content = f.read() + filename = os.path.basename(filepath) + filetype = os.path.splitext(filename)[-1][1:].upper() + mimes = { + 'PDF': 'application/pdf', + 'ODF': 'application/odf' + } + if filetype not in mimes: + raise Exception(f'wechatpayv3 does not support this file type: {filetype}') + params = {} + params.update({'meta': '{"file_type":"%s","digest_alogrithm":"SM3","digest":"%s"}' % (filetype, sm3(content))}) + files = [('file', (filename, content, mimes[filetype]))] + path = '/v3/new-tax-control-fapiao/fapiao-applications/upload-fapiao-file' + return self._core.request(path, method=RequestType.POST, data=params, sign_data=params.get('meta'), files=files) + + +def fapiao_insert_cards(self, fapiao_apply_id, buyer_information, fapiao_card_information, scene='WITH_WECHATPAY'): + """将电子发票插入微信用户卡包 + :param fapiao_apply_id: 发票申请单号,示例值:'4200000444201910177461284488' + :param buyer_information: 购买方信息,即发票抬头。示例值:{'type':'ORGANIZATION','name':'深圳市南山区测试企业'} + :param fapiao_card_information: 电子发票卡券信息列表,最多五条。示例值:[{'fapiao_media_id':'ASNFZ4mrze/+3LqYdlQyEA==','fapiao_number':'123456','fapiao_code':'044001911211','fapiao_time':'2020-07-01T12:00:00+08:00','check_code':'69001808340631374774'......}] + :param scene: 场景值,目前只支持WITH_WECHATPAY。示例值:'WITH_WECHATPAY' + """ + if fapiao_apply_id: + path = '/v3/new-tax-control-fapiao/fapiao-applications/%s/insert-cards' % fapiao_apply_id + else: + raise Exception('fapiao_apply_id is not assigned.') + params = {} + if buyer_information: + params.update({'buyer_information': buyer_information}) + else: + raise Exception('buyer_information is not assigned.') + if fapiao_card_information: + params.update({'fapiao_card_information': fapiao_card_information}) + else: + raise Exception('fapiao_card_information is not assigned.') + params.update({'scene': scene}) + return self._core.request(path, method=RequestType.POST, data=params) + + +def fapiao_check_submch(self, sub_mchid): + """检查子商户开票功能状态 + :param sub_mch: 子商户号,微信支付分配的子商户号。示例值:'1900000001' + """ + if sub_mchid: + path = '/v3/new-tax-control-fapiao/merchant/%s/check' % sub_mchid + else: + raise Exception('sub_mchid is not assigned.') + return self._core.request(path) + + +def fapiao_query_files(self, fapiao_apply_id, sub_mchid=None, fapiao_id=None): + """获取发票下载信息 + :param fapiao_apply_id: 发票申请单号,开票时指定的发票申请单号。 + :param sub_mchid: 子商户号,微信支付分配的子商户号。示例值:'1900000001' + :param fapiao_id: 商户发票单号,开票时指定的商户发票单号,唯一标识一张电子发票。 + """ + if fapiao_apply_id: + path = '/v3/new-tax-control-fapiao/fapiao-applications/%s/fapiao-files' % fapiao_apply_id + else: + raise Exception('fapiao_apply_id is not assigned.') + params = {} + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + if fapiao_id: + params.update({'fapiao_id': fapiao_id}) + return self._core.request(path, method=RequestType.POST, data=params) + + +def fapiao_download_file(self, url, openid, invoice_code, invoice_no, fapiao_id, sub_mchid=None): + """下载发票文件 + :param url: 获取发票下载信息接口返回的download_url,保留其中的token字段不要删除。 + """ + if not (url and openid and invoice_code and invoice_no and fapiao_id): + raise Exception('url, openid, invoice_code, invocide_no or fapiao_id is not assigned.') + else: + path = '%s&mchid=%s&openid=%s&invoice_code=%s&invoice_no=%s&fapiao_id=%s' % (url, self._mchid, openid, invoice_code, invoice_no, fapiao_id) + if self._partner_mode: + if sub_mchid: + path = '%s&sub_mchid=%s' % (path, sub_mchid) + else: + raise Exception('sub_mchid is not assigned.') + return self._core.request(path) diff --git a/wechatpayv3/goldplan.py b/wechatpayv3/goldplan.py new file mode 100644 index 0000000..9ed16b4 --- /dev/null +++ b/wechatpayv3/goldplan.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType + + +def goldplan_plan_change(self, sub_mchid, operation_type): + """点金计划管理 + :param sub_mchid: 子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + :param operation_type: 操作类型, 枚举值:'OPEN':表示开通点金计划,'CLOSE':表示关闭点金计划。示例值:'OPEN' + """ + params = {} + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + if operation_type: + params.update({'operation_type': operation_type}) + else: + raise Exception('operation_type is not assigned.') + path = '/v3/goldplan/merchants/changegoldplanstatus' + return self._core.request(path, method=RequestType.POST, data=params) + + +def goldplan_custompage_change(self, sub_mchid, operation_type): + """商家小票管理 + :param sub_mchid: 子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + :param operation_type: 操作类型, 枚举值:'OPEN':表示开通商家自定义小票,'CLOSE':表示关闭商家自定义小票。示例值:'OPEN' + """ + params = {} + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + if operation_type: + params.update({'operation_type': operation_type}) + else: + raise Exception('operation_type is not assigned.') + path = '/v3/goldplan/merchants/changecustompagestatus' + return self._core.request(path, method=RequestType.POST, data=params) + + +def goldplan_advertising_filter(self, sub_mchid, advertising_industry_filters): + """同业过滤标签管理 + :param sub_mchid: 子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + :param advertising_industry_filters: 同业过滤标签值, 同业过滤标签最少传一个,最多三个。示例值:['SOFTWARE','SECURITY','LOVE_MARRIAGE'] + """ + params = {} + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + if advertising_industry_filters: + params.update({'advertising_industry_filters': advertising_industry_filters}) + else: + raise Exception('advertising_industry_filters is not assigned.') + path = '/v3/goldplan/merchants/set-advertising-industry-filter' + return self._core.request(path, method=RequestType.POST, data=params) + + +def goldplan_advertising_open(self, sub_mchid, advertising_industry_filters=None): + """开通广告展示 + :param sub_mchid: 子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + :param advertising_industry_filters: 同业过滤标签值, 同业过滤标签最少传一个,最多三个。示例值:['SOFTWARE','SECURITY','LOVE_MARRIAGE'] + """ + params = {} + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + if advertising_industry_filters: + params.update({'advertising_industry_filters': advertising_industry_filters}) + else: + raise Exception('advertising_industry_filters is not assigned.') + path = '/v3/goldplan/merchants/open-advertising-show' + return self._core.request(path, method=RequestType.PATCH, data=params) + + +def goldplan_advertising_close(self, sub_mchid): + """关闭广告展示 + :param sub_mchid: 子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + path = '/v3/goldplan/merchants/close-advertising-show' + return self._core.request(path, method=RequestType.POST, data=params) diff --git a/wechatpayv3/marketing.py b/wechatpayv3/marketing.py new file mode 100644 index 0000000..bfdef06 --- /dev/null +++ b/wechatpayv3/marketing.py @@ -0,0 +1,1070 @@ +# -*- coding: utf-8 -*- + +import os + +from .media import _media_upload +from .type import RequestType +from .utils import sha256 + + +def marketing_image_upload(self, filepath, filename=None): + """图片上传(营销专用) + :param filepath: 图片文件路径 + :param filename: 文件名称,未指定则从filepath参数中截取 + """ + return _media_upload(self, filepath, filename, '/v3/marketing/favor/media/image-upload') + + +def marketing_card_send(self, card_id, openid, out_request_no, send_time, appid=None): + """发放消费卡 + :card_id: 消费卡ID。示例值:'pIJMr5MMiIkO_93VtPyIiEk2DZ4w' + :openid: 用户openid,待发卡用户的openid。示例值:'obLatjhnqgy2syxrXVM3MJirbkdI' + :out_request_no: 商户单据号。示例值:'oTYhjfdsahnssddj_0136' + :send_time: 请求发卡时间,单次请求发卡时间,消费卡在商户系统的实际发放时间,为东八区标准时间(UTC+8)。示例值:'2019-12-31T13:29:35.120+08:00' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if card_id: + path = '/v3/marketing/busifavor/coupons/%s/send' % card_id + else: + raise Exception('card_id is not assigned.') + if openid: + params.update({'openid': openid}) + else: + raise Exception('openid is not assigned.') + if out_request_no: + params.update({'out_request_no': out_request_no}) + else: + raise Exception('out_request_no is not assigned.') + if send_time: + params.update({'send_time': send_time}) + else: + raise Exception('send_time is not assigned.') + params.update({'appid': appid or self._appid}) + return self._core.request(path, method=RequestType.POST, data=params) + + +def marketing_partnership_build(self, idempotency_key, partner_type, business_type, partner_appid=None, + partner_merchant_id=None, stock_id=None): + """建立合作关系 + :idempotency_key: 业务请求幂等值,商户侧需保持唯一性,可包含英文字母,数字,|,_,*,-等内容,不允许出现其他不合法符号。示例值:'12345' + :partner_type: 合作方类别,枚举值:'APPID':合作方为APPID,'MERCHANT':合作方为商户。示例值:'APPID' + :business_type: 授权业务类别,枚举值:'FAVOR_STOCK':代金券批次,'BUSIFAVOR_STOCK':商家券批次。示例值:'FAVOR_STOCK' + :partner_appid: 合作方APPID,合作方类别为APPID时必填。示例值:'wx4e1916a585d1f4e9' + :partner_merchant_id: 合作方商户ID,合作方类别为MERCHANT时必填。特殊规则:最小字符长度为8。示例值:'2480029552' + :stock_id: 授权批次ID,授权业务类别为商家券批次或代金券批次时,此参数必填。示例值:'2433405' + """ + headers = {} + if idempotency_key: + headers.update({'Idempotency-Key': idempotency_key}) + else: + raise Exception('idempotency_key is not assigned.') + params = {} + if partner_type == 'APPID' and partner_appid: + params.update({'partner': {'type': partner_type, 'appid': partner_appid}}) + elif partner_type == 'MERCHANT' and partner_merchant_id: + params.update({'partner': {'type': partner_type, 'merchant_id': partner_merchant_id}}) + else: + raise Exception('invalid value in partner_type/partner_appid/partner_merchant_id') + if business_type not in ['FAVOR_STOCK', 'BUSIFAVOR_STOCK'] or not stock_id: + raise Exception('invalid value in bussiness_type/stock_id.') + params.update({'authorized_data': {'bussiness_type': business_type, 'stock_id': stock_id}}) + path = '/v3/marketing/partnerships/build' + return self._core.request(path, method=RequestType.POST, data=params, headers=headers) + + +def marketing_partnership_query(self, business_type, stock_id, partner_type=None, partner_appid=None, + partner_merchant_id=None, limit=20, offset=None): + """查询合作关系列表 + :business_type: 授权业务类别,枚举值:'FAVOR_STOCK':代金券批次,'BUSIFAVOR_STOCK':商家券批次。示例值:'FAVOR_STOCK' + :stock_id: 授权批次ID,授权业务类别为商家券批次或代金券批次时,此参数必填。示例值:'2433405' + :partner_type: 合作方类别,枚举值:'APPID':合作方为APPID,'MERCHANT':合作方为商户。示例值:'APPID' + :partner_appid: 合作方APPID,合作方类别为APPID时必填。示例值:'wx4e1916a585d1f4e9' + :partner_merchant_id: 合作方商户ID,合作方类别为MERCHANT时必填。特殊规则:最小字符长度为8。示例值:'2480029552' + :limit: 分页大小,最大50。不传默认为20。示例值:5 + :offset: 分页页码,页码从0开始。示例值:10 + """ + path = '/v3/marketing/partnerships?' + if business_type not in ['FAVOR_STOCK', 'BUSIFAVOR_STOCK'] or not stock_id: + raise Exception('invalid value in bussiness_type/stock_id.') + path = '%sauthorized_data={"business_type":"%s","stock_id":"%s"}' % (path, business_type, stock_id) + if partner_type == 'APPID' and partner_appid: + path = '%s&partner={"type":"%s","appid":"%s"}' % (path, partner_type, partner_appid) + elif partner_type == 'MERCHANT' and partner_merchant_id: + path = '%s&partner={"type":"%s","merchant_id":"%s"}' % (path, partner_type, partner_merchant_id) + if limit in range(0, 51): + path = '%s&limit=%s' % (path, limit) + if offset: + path = '%s&offset=%s' % (path, offset) + return self._core.request(path) + + +def marketing_paygift_activity_create(self, activity_base_info, award_send_rule, advanced_setting=None): + """创建全场满额送活动 + :param activity_base_info: 活动基本信息 + :param award_send_rule: 活动奖品发放规则 + :param advanced_setting: 活动高级设置 + """ + params = {} + if not activity_base_info or not award_send_rule: + raise Exception('activity_base_info or award_send_rule is not assigned.') + params.update({'activity_base_info': activity_base_info}) + params.update({'award_send_rule': award_send_rule}) + if advanced_setting: + params.update({'advanced_setting': advanced_setting}) + path = '/v3/marketing/paygiftactivity/unique-threshold-activity' + return self._core.request(path, method=RequestType.POST, data=params) + + +def marketing_paygift_activity_detail(self, activity_id): + """查询活动详情接口 + :param activity_id: 活动id,示例值:'10028001' + """ + if activity_id: + path = '/v3/marketing/paygiftactivity/activities/%s' % activity_id + else: + raise Exception('activity_id is not assigned.') + return self._core.request(path) + + +def marketing_paygift_merchants_list(self, activity_id, offset=0, limit=20): + """查询活动发券商户号 + :param activity_id: 活动id,示例值:'10028001' + :param offset:分页页码,页面从0开始。示例值:1 + :param limit: 分页大小,限制分页最大数据条目。示例值:20 + """ + if activity_id: + path = '/v3/marketing/paygiftactivity/activities/%s/merchants' % activity_id + else: + raise Exception('activity_id is not assigned.') + path = '%s?offset=%s&limit=%s' % (path, offset, limit) + return self._core.request(path) + + +def marketing_paygift_goods_list(self, activity_id, offset=0, limit=20): + """查询活动指定商品列表 + :param activity_id: 活动id,示例值:'10028001' + :param offset:分页页码,页面从0开始。示例值:1 + :param limit: 分页大小,限制分页最大数据条目。示例值:20 + """ + if activity_id: + path = '/v3/marketing/paygiftactivity/activities/%s/goods' % activity_id + else: + raise Exception('activity_id is not assigned.') + path = '%s?offset=%s&limit=%s' % (path, offset, limit) + return self._core.request(path) + + +def marketing_paygift_activity_terminate(self, activity_id): + """终止活动 + :param activity_id: 活动id,示例值:'10028001' + """ + if activity_id: + path = '/v3/marketing/paygiftactivity/activities/%s/terminate' % activity_id + else: + raise Exception('activity_id is not assigned.') + return self._core.request(path, method=RequestType.POST) + + +def marketing_paygift_merchant_add(self, activity_id, add_request_no, merchant_id_list=[]): + """新增活动发券商户号 + :param activity_id: 活动id,示例值:'10028001' + :param add_request_no: 请求业务单据号,商户添加发券商户号的凭据号,商户侧需保持唯一性。示例值:'100002322019090134234sfdf' + :param merchant_id_list: 发券商户号,新增到活动中的发券商户号列表,特殊规则:最小字符长度为8,最大为15,条目个数限制:[1,500]。示例值:["10000022","10000023"] + """ + if activity_id: + path = '/v3/marketing/paygiftactivity/activities/%s/merchants/add' % activity_id + else: + raise Exception('activity_id is not assigned.') + params = {} + if add_request_no: + params.update({'add_request_no': add_request_no}) + else: + raise Exception('add_request_no is not assigned.') + if merchant_id_list: + params.update({'merchant_id_list': merchant_id_list}) + return self._core.request(path, method=RequestType.POST, data=params) + + +def marketing_paygift_activity_list(self, offset=0, limit=20, activity_name=None, activity_status=None, award_type=None): + """获取支付有礼活动列表 + :param offset:分页页码,页面从0开始。示例值:1 + :param limit: 分页大小,限制分页最大数据条目。示例值:20 + :param activity_name: 活动名称,支持模糊搜索。示例值:'良品铺子回馈活动' + :param activity_status: 活动状态,枚举值:'ACT_STATUS_UNKNOWN':状态未知,'CREATE_ACT_STATUS':已创建,'ONGOING_ACT_STATUS':运行中,'TERMINATE_ACT_STATUS':已终止, + 'STOP_ACT_STATUS':已暂停,'OVER_TIME_ACT_STATUS':已过期,'CREATE_ACT_FAILED':创建活动失败。示例值:'CREATE_ACT_STATUS' + :param award_type: 奖品类型,暂时只支持商家券。'BUSIFAVOR':商家券。示例值:'BUSIFAVOR' + """ + params = {} + params.update({'offset': offset}) + params.update({'limit': limit}) + if activity_name: + params.update({'activity_name': activity_name}) + if activity_status: + params.update({'activity_status': activity_status}) + if award_type: + params.update({'award_type': award_type}) + path = '/v3/marketing/paygiftactivity/activities' + return self._core.request(path, method=RequestType.POST, data=params) + + +def marketing_paygift_merchant_delete(self, activity_id, merchant_id_list=[], delete_request_no=None): + """删除活动发券商户号 + :param activity_id: 活动id,示例值:'10028001' + :param delete_request_no: 请求业务单据号,商户创建批次凭据号(格式:商户id+日期+流水号),商户侧需保持唯一性,可包含英文字母,数字,|,_,*,-等内容,不允许出现其他不合法符号。示例值:'100002322019090134234sfdf' + :param merchant_id_list: 删除的发券商户号,从活动已有的发券商户号中移除的商户号列表,特殊规则:最小字符长度为8,最大为15,条目个数限制:[1,500]。示例值:["10000022","10000023"] + """ + if activity_id: + path = '/v3/marketing/paygiftactivity/activities/%s/merchants/delete' % activity_id + else: + raise Exception('activity_id is not assigned.') + params = {} + if merchant_id_list: + params.update({'merchant_id_list': merchant_id_list}) + if delete_request_no: + params.update({'delete_request_no': delete_request_no}) + return self._core.request(path, method=RequestType.POST, data=params) + + +def marketing_favor_stock_create(self, + stock_name, + belong_merchant, + available_begin_time, + available_end_time, + stock_use_rule, + coupon_use_rule, + out_request_no, + stock_type='NORMAL', + no_cash=False, + comment=None, + pattern_info=None, + ext_info=None): + """创建代金券批次 + :param stock_name: 批次名称,示例值:'微信支付代金券批次' + :param belong_merchant: 归属商户号。示例值:'98568865' + :param available_begin_time: 可用时间-开始时间,格式为YYYY-MM-DDTHH:mm:ss.sss+TIMEZONE。示例值:'2015-05-20T13:29:35.120+08:00' + :param available_end_time: 可用时间-结束时间,格式为YYYY-MM-DDTHH:mm:ss.sss+TIMEZONE。示例值:'2015-05-20T13:29:35.120+08:00' + :param stock_use_rule: 发放规则。示例值:{'max_coupons':5, 'max_amount':100, 'max_coupons_per_user':1, 'natural_person_limit':False, 'prevent_api_abuse':True} + :param coupon_use_rule: 核销规则。示例值:{'available_merchants':['9856000','9856111']} + :param out_request_no: 商户单据号,可包含英文字母,数字,|,_,*,-等内容,不允许出现其他不合法符号,商户侧需保持商户单据号全局唯一。示例值:'89560002019101000121' + :param stock_type: 批次类型,仅支持:'NORMAL':固定面额满减券批次。示例值:'NORMAL' + :param no_cash: 营销经费,枚举值:True:免充值,False:预充值。示例值:False + :param comment: 批次备注,仅制券商户可见,用于自定义信息。校验规则:批次备注最多60个UTF8字符数。示例值:'零售批次' + :param pattern_info: 样式设置,示例值:{'description':'微信支付营销代金券'} + :param ext_info: 扩展属性,json格式字符串,如无需要则不填写。示例值:"{'exinfo1':'1234','exinfo2':'3456'}" + """ + params = {} + if stock_name: + params.update({'stock_name': stock_name}) + else: + raise Exception('stock_name is not assigned.') + if belong_merchant: + params.update({'belong_merchant': belong_merchant}) + else: + raise Exception('belong_merchant is not assigned.') + if available_begin_time: + params.update({'available_begin_time': available_begin_time}) + else: + raise Exception('available_begin_time is not assigned.') + if available_end_time: + params.update({'available_end_time': available_end_time}) + else: + raise Exception('available_end_time is not assigned.') + if stock_use_rule: + params.update({'stock_use_rule': stock_use_rule}) + else: + raise Exception('stock_use_rule is not assigned.') + if coupon_use_rule: + params.update({'coupon_use_rule': coupon_use_rule}) + else: + raise Exception('coupon_use_rule is not assigned.') + if stock_type: + params.update({'stock_type': stock_type}) + else: + raise Exception('stock_type is not assigned.') + if out_request_no: + params.update({'out_request_no': out_request_no}) + else: + raise Exception('out_request_no is not assigned.') + if no_cash: + params.update({'no_cash': no_cash}) + if comment: + params.update({'comment': comment}) + if pattern_info: + params.update({'pattern_info': pattern_info}) + if ext_info: + params.update({'ext_info': ext_info}) + path = '/v3/marketing/favor/coupon-stocks' + return self._core.request(path, method=RequestType.POST, data=params) + + +def marketing_favor_stock_start(self, stock_creator_mchid, stock_id): + """激活代金券批次 + :param stock_creator_mchid: 创建批次的商户号,示例值:'8956000' + :param stock_id: 批次号,示例值:'9856000' + """ + params = {} + if stock_creator_mchid: + params.update({'stock_creator_mchid': stock_creator_mchid}) + else: + raise Exception('stock_creator_mchid is not assigned.') + if stock_id: + path = '/v3/marketing/favor/stocks/%s/start' % stock_id + else: + raise Exception('stock_id is not assigned.') + return self._core.request(path, method=RequestType.POST, data=params) + + +def marketing_favor_stock_send(self, + stock_id, + openid, + out_request_no, + stock_creator_mchid, + coupon_value=None, + coupon_minimum=None, + appid=None): + """发放代金券批次 + :param stock_id: 批次号。示例值:'9856000' + :param openid: 用户openid,示例值:'2323dfsdf342342' + :param out_request_no: 商户单据号,示例值: '89560002019101000121' + :param stock_creator_mchid: 创建批次的商户号,示例值:'8956000' + :param coupon_value: 指定面额发券,面额。示例值:100 + :param coupon_minimum: 指定面额发券,券门槛。示例值:100 + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if stock_id: + params.update({'stock_id': stock_id}) + else: + raise Exception('stock_id is not assigned.') + if openid: + path = '/v3/marketing/favor/users/%s/coupons' % openid + else: + raise Exception('openid is not assigned.') + if out_request_no: + params.update({'out_request_no': out_request_no}) + else: + raise Exception('out_request_no is not assigned.') + if stock_creator_mchid: + params.update({'stock_creator_mchid': stock_creator_mchid}) + else: + raise Exception('stock_creator_mchid is not assigned.') + if coupon_value: + params.update({'coupon_value': coupon_value}) + if coupon_minimum: + params.update({'coupon_minimum': coupon_minimum}) + params.update({'appid': appid or self._appid}) + return self._core.request(path, method=RequestType.POST, data=params) + + +def marketing_favor_stock_pause(self, stock_creator_mchid, stock_id): + """暂停代金券批次 + :param stock_creator_mchid: 创建批次的商户号,示例值:'8956000' + :param stock_id: 批次号,示例值:'9856000' + """ + params = {} + if stock_creator_mchid: + params.update({'stock_creator_mchid': stock_creator_mchid}) + else: + raise Exception('stock_creator_mchid is not assigned.') + if stock_id: + path = '/v3/marketing/favor/stocks/%s/pause' % stock_id + else: + raise Exception('stock_id is not assigned.') + return self._core.request(path, method=RequestType.POST, data=params) + + +def marketing_favor_stock_restart(self, stock_creator_mchid, stock_id): + """重启代金券批次 + :param stock_creator_mchid: 创建批次的商户号,示例值:'8956000' + :param stock_id: 批次号,示例值:'9856000' + """ + params = {} + if stock_creator_mchid: + params.update({'stock_creator_mchid': stock_creator_mchid}) + else: + raise Exception('stock_creator_mchid is not assigned.') + if stock_id: + path = '/v3/marketing/favor/stocks/%s/restart' % stock_id + else: + raise Exception('stock_id is not assigned.') + return self._core.request(path, method=RequestType.POST, data=params) + + +def marketing_favor_stock_list(self, + stock_creator_mchid, + offset=0, + limit=10, + create_start_time=None, + create_end_time=None, + status=None): + """条件查询批次列表 + :param stock_creator_mchid: 创建批次的商户号,示例值:'8956000' + :param offset: 分页页码,页码从0开始,默认第0页。示例值:0 + :param limit: 分页大小,最大10。示例值:8 + :param create_start_time: 起始创建时间,格式为YYYY-MM-DDTHH:mm:ss.sss+TIMEZONE。示例值:'2015-05-20T13:29:35.120+08:00' + :param create_end_time: 终止创建时间,格式为YYYY-MM-DDTHH:mm:ss.sss+TIMEZONE。示例值:'2015-05-20T13:29:35.120+08:00' + :param status: 批次状态,枚举值:'unactivated':未激活,'audit':审核中,'running':运行中,'stoped':已停止,'paused':暂停发放。示例值:'paused' + """ + if stock_creator_mchid: + path = '/v3/marketing/favor/stocks?offset=%s&limit=%s&stock_creator_mchid=%s' % (offset, limit, stock_creator_mchid) + else: + raise Exception('stock_creator_mchid is not assigned.') + if create_start_time: + path = '%s&create_start_time=%s' % (path, create_start_time) + if create_end_time: + path = '%s&create_end_time=%s' % (path, create_end_time) + if status: + path = '%s&status=%s' % (path, status) + return self._core.request(path) + + +def marketing_favor_stock_detail(self, stock_creator_mchid, stock_id): + """查询批次详情 + :param stock_creator_mchid: 创建批次的商户号,示例值:'8956000' + :param stock_id: 批次号,示例值:'9856000' + """ + if stock_id: + path = '/v3/marketing/favor/stocks/%s' % stock_id + else: + raise Exception('stock_id is not assigned.') + if stock_creator_mchid: + path = '%s?stock_creator_mchid=%s' % (path, stock_creator_mchid) + else: + raise Exception('stock_creator_mchid is not assigned.') + return self._core.request(path) + + +def marketing_favor_coupon_detail(self, coupon_id, openid): + """查询代金券详情 + :param coupon_id: 代金券id,示例值:'9856888' + :param openid: 用户openid,示例值:'2323dfsdf342342' + """ + if coupon_id and openid: + path = '/v3/marketing/favor/users/%s/coupons/%s?appid=%s' % (openid, coupon_id, self._appid) + else: + raise Exception('coupon_id or openid is not assigned.') + return self._core.request(path) + + +def marketing_favor_stock_merchant(self, stock_creator_mchid, stock_id, offset=0, limit=50): + """查询代金券可用商户 + :param stock_creator_mchid: 创建批次的商户号,示例值:'8956000' + :param stock_id: 批次号,示例值:'9856000' + :param offset: 分页页码,最大1000。示例值: 10 + :param limit: 分页大小,最大50。示例值: 10 + """ + if stock_id: + path = '/v3/marketing/favor/stocks/%s/merchants' % stock_id + else: + raise Exception('stock_id is not assigned.') + if stock_creator_mchid: + path = '%s?stock_creator_mchid=%s&offset=%s&limit=%s&' % (path, stock_creator_mchid, offset, limit) + return self._core.request(path) + + +def marketing_favor_stock_item(self, stock_creator_mchid, stock_id, offset=0, limit=50): + """查询代金券可用单品 + :param stock_creator_mchid: 创建批次的商户号,示例值:'8956000' + :param stock_id: 批次号,示例值:'9856000' + :param offset: 分页页码,最大500。示例值: 10 + :param limit: 分页大小,最大100。示例值: 10 + """ + if stock_id: + path = '/v3/marketing/favor/stocks/%s/items' % stock_id + else: + raise Exception('stock_id is not assigned.') + if stock_creator_mchid: + path = '%s?stock_creator_mchid=%s&offset=%s&limit=%s&' % (path, stock_creator_mchid, offset, limit) + return self._core.request(path) + + +def marketing_favor_user_coupon(self, + openid, + stock_id=None, + status=None, + creator_mchid=None, + sender_mchid=None, + available_mchid=None, + offset=0, + limit=20): + """根据商户号查用户的券 + :param openid: 用户openid,示例值:'2323dfsdf342342' + :param stock_id: 批次号,示例值:'9856000' + :param status: 券状态,代金券状态:'SENDED':可用,'USED':已实扣,填写available_mchid参数则该字段不生效。示例值:'USED' + :param creator_mchid: 创建批次的商户号.示例值:'9865002' + :param sender_mchid: 批次发放商户号。示例值:'9865001' + :param available_mchid: 可用商户号。示例值: '9865000' + :param offset: 分页页码,默认0,填写available_mchid,该字段不生效。示例值:0 + :param limit: 分页大小,默认20,填写available_mchid,该字段不生效。示例值:20 + """ + if openid: + path = '/v3/marketing/favor/users/%s/coupons?appid=%s&offset=%s&limit=%s' % (openid, self._appid, offset, limit) + else: + raise Exception('openid is not assigned.') + if stock_id: + path = '%s&stock_id=%s' % (path, stock_id) + if status: + path = '%s&status=%s' % (path, status) + if creator_mchid: + path = '%s&creator_mchid=%s' % (path, creator_mchid) + elif sender_mchid: + path = '%s&sender_mchid=%s' % (path, sender_mchid) + elif available_mchid: + path = '%s&available_mchid=%s' % (path, available_mchid) + return self._core.request(path) + + +def marketing_favor_use_flow(self, stock_id): + """下载批次核销明细 + :param stock_id: 批次号,微信为每个代金券批次分配的唯一id。示例值:'9865000' + """ + if stock_id: + path = '/v3/marketing/favor/stocks/%s/use-flow' % stock_id + else: + raise Exception('stock_id is not assigned.') + return self._core.request(path) + + +def marketing_favor_refund_flow(self, stock_id): + """下载批次退款明细 + :param stock_id: 批次号,微信为每个代金券批次分配的唯一id。示例值:'9865000' + """ + if stock_id: + path = '/v3/marketing/favor/stocks/%s/refund-flow' % stock_id + else: + raise Exception('stock_id is not assigned.') + return self._core.request(path) + + +def marketing_favor_callback_update(self, notify_url=None, switch=True, mchid=None): + """设置消息通知地址 + :param notify_url: 支付通知商户url地址。示例值:'https://pay.weixin.qq.com' + :param switch: 回调开关,枚举值:True:开启推送,False:停止推送。示例值:True + :param mchid: 微信支付商户号,可不填,默认传入初始化的mchid。示例值:'9856888' + """ + params = {} + params.update({'mchid': mchid or self._mchid}) + if not (notify_url or self._notify_url): + raise Exception('notify_url is not assigned.') + params.update({'notify_url': notify_url or self._notify_url}) + params.update({'switch': switch}) + path = '/v3/marketing/favor/callbacks' + return self._core.request(path, method=RequestType.POST, data=params) + + +def marketing_busifavor_stock_create(self, + stock_name, + belong_merchant, + goods_name, + stock_type, + coupon_use_rule, + stock_send_rule, + out_request_no, + coupon_code_mode, + comment=None, + custom_entrance=None, + display_pattern_info=None, + notify_config=None, + subsidy=False): + """创建商家券 + :params stock_name: 商家券批次名称,字数上限为21个,一个中文汉字/英文字母/数字均占用一个字数。示例值:'8月1日活动券' + :params belong_merchant: 批次归属商户号。注:普通直连模式,该参数为直连商户号。示例值:'10000022' + :params goods_name: 适用商品范围,用来描述批次在哪些商品可用,会显示在微信卡包中。字数上限为15个。示例值:'xxx商品使用' + :params stock_type: 批次类型,'NORMAL':固定面额满减券批次,'DISCOUNT':折扣券批次,'EXCHANGE':换购券批次。示例值:'NORMAL' + :params coupon_use_rule: 核销规则。示例值:{'coupon_available_time':{}, 'fixed_normal_coupon':{}, 'use_method':'OFF_LINE', } + :params stock_send_rule: 发放规则。示例值:{'max_coupons':100, 'max_coupons_per_user':5} + :params out_request_no: 商户请求单号。示例值:'100002322019090134234sfdf' + :params coupon_code_mode: 券code模式,枚举值:'WECHATPAY_MODE':系统分配券code。(固定22位纯数字),'MERCHANT_API':商户发放时接口指定券code,'MERCHANT_UPLOAD':商户上传自定义code,发券时系统随机选取上传的券code。示例值:'WECHATPAY_MODE' + :params comment: 批次备注,仅配置商户可见,用于自定义信息。字数上限为20个。示例值:'活动使用' + :params custom_entrance: 自定义入口。示例值:{'hall_id':'233455656'} + :params display_pattern_info: 样式信息。示例值:{'description':'xxx门店可用'} + :params notify_config: 事件通知配置。示例值:{'notify_appid':'wx23232232323'} + :params subsidy=False: 是否允许营销补贴,该批次发放的券是否允许进行补差。示例值:False + """ + params = {} + if stock_name: + params.update({'stock_name': stock_name}) + else: + raise Exception('stock_name is not assigned.') + if belong_merchant: + params.update({'belong_merchant': belong_merchant}) + else: + raise Exception('belong_merchant is not assigned.') + if goods_name: + params.update({'goods_name': goods_name}) + else: + raise Exception('goods_name is not assigned.') + if stock_type: + params.update({'stock_type': stock_type}) + else: + raise Exception('stock_type is not assigned.') + if coupon_use_rule: + params.update({'coupon_use_rule': coupon_use_rule}) + else: + raise Exception('coupon_use_rule is not assigned.') + if stock_send_rule: + params.update({'stock_send_rule': stock_send_rule}) + else: + raise Exception('stock_send_rule is not assigned.') + if out_request_no: + params.update({'out_request_no': out_request_no}) + else: + raise Exception('out_request_no is not assigned.') + if coupon_code_mode: + params.update({'coupon_code_mode': coupon_code_mode}) + else: + raise Exception('coupon_code_mode is not assigned.') + if comment: + params.update({'comment': comment}) + if custom_entrance: + params.update({'custom_entrance': custom_entrance}) + if display_pattern_info: + params.update({'display_pattern_info': display_pattern_info}) + if notify_config: + params.update({'notify_config': notify_config}) + params.update({'subsidy': subsidy}) + path = '/v3/marketing/busifavor/stocks' + return self._core.request(path, method=RequestType.POST, data=params) + + +def marketing_busifavor_stock_query(self, stock_id): + """查询商家券详情 + :param stock_id: 批次号。示例值:1212 + """ + if stock_id: + path = '/v3/marketing/busifavor/stocks/%s' % stock_id + else: + raise Exception('stock_id is not assigned.') + return self._core.request(path) + + +def marketing_busifavor_coupon_use(self, + coupon_code, + use_time, + use_request_no, + stock_id=None, + openid=None, + appid=None): + """核销用户券 + :param coupon_code: 券code,券的唯一标识。示例值:'sxxe34343434' + :param use_time: 请求核销时间,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE。示例值:'2015-05-20T13:29:35+08:00' + :param use_request_no: 核销请求单据号,每次核销请求的唯一标识,商户需保证唯一。示例值:'1002600620019090123143254435' + :param stock_id: 批次号。示例值:1212 + :param openid: 用户标识。示例值:'xsd3434454567676' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if coupon_code: + params.update({'coupon_code': coupon_code}) + else: + raise Exception('coupon_code is not assigned.') + if use_time: + params.update({'use_time': use_time}) + else: + raise Exception('use_time is not assigned.') + if use_request_no: + params.update({'use_request_no': use_request_no}) + else: + raise Exception('use_request_no is not assigned.') + if stock_id: + params.update({'stock_id': stock_id}) + if openid: + params.update({'openid': openid}) + params.update({'appid': appid or self._appid}) + path = '/v3/marketing/busifavor/coupons/use' + return self._core.request(path, method=RequestType.POST, data=params) + + +def marketing_busifavor_user_coupon(self, + openid, + stock_id=None, + coupon_state=None, + creator_merchant=None, + belong_merchant=None, + sender_merchant=None, + offset=0, + limit=20): + """根据过滤条件查询用户券 + :param openid: 用户标识。示例值:'xsd3434454567676' + :param stock_id: 批次号。示例值:1212 + :param coupon_state: 券状态,枚举值:'SENDED':可用,'USED':已核销,'EXPIRED':已过期,示例值:'SENDED' + :param creator_merchant: 创建批次的商户号。示例值:'1000000001' + :param belong_merchant: 批次归属商户号。示例值:'1000000002' + :param sender_merchant: 批次发放商户号。示例值:'1000000003' + :param offset: 分页页码。示例值:0 + :param limit: 分页大小。示例值:20 + """ + if openid: + path = '/v3/marketing/busifavor/users/%s/coupons?appid=%s&offset=%s&limit=%s' % (openid, self._appid, offset, limit) + else: + raise Exception('openid is not assigned.') + if stock_id: + path = '%s&stock_id=%s' % (path, stock_id) + if coupon_state: + path = '%s&coupon_state=%s' % (path, coupon_state) + if creator_merchant: + path = '%s&creator_merchant=%s' % (path, creator_merchant) + if belong_merchant: + path = '%s&belong_merchant=%s' % (path, belong_merchant) + if sender_merchant: + path = '%s&sender_merchant=%s' % (path, sender_merchant) + return self._core.request(path) + + +def marketing_busifavor_coupon_detail(self, coupon_code, openid): + """查询用户单张券详情 + :param coupon_code: 券code,券的唯一标识。示例值:'sxxe34343434' + :param openid: 用户标识。示例值:'xsd3434454567676' + """ + if not (coupon_code and openid): + raise Exception('coupon_code or openid is not assigned.') + path = '/v3/marketing/busifavor/users/%s/coupons/%s/appids/%s' % (openid, coupon_code, self._appid) + return self._core.request(path) + + +def marketing_busifavor_couponcode_upload(self, + stock_id, + upload_request_no, + coupon_code_list=[]): + """上传预存code + :param stock_id: 批次号。示例值:1212 + :param upload_request_no: 请求业务单据号。商户上传code的凭据号,商户侧需保持唯一性。示例值:'100002322019090134234sfdf' + :param coupon_code_list: 券code列表。示例值:['ABC9588200','ABC9588201'] + """ + params = {} + if stock_id: + path = '/v3/marketing/busifavor/stocks/%s/couponcodes' % stock_id + else: + raise Exception('stock_id is not assigned.') + if upload_request_no: + params.update({'upload_request_no': upload_request_no}) + else: + raise Exception('upload_request_no is not assigned.') + if coupon_code_list: + params.update({'coupon_code_list': coupon_code_list}) + return self._core.request(path, method=RequestType.POST, data=params) + + +def marketing_busifavor_callback_update(self, mchid=None, notify_url=None): + """设置商家券事件通知地址 + :param mchid: 商户号,可不填,默认传入初始化的mchid。示例值:'10000098' + :param notify_url: 通知URL地址,用于接收商家券事件通知的url地址,不填默认使用初始化的notify_url。示例值:'https://pay.weixin.qq.com' + """ + params = {} + params.update({'mchid': mchid or self._mchid}) + if not (notify_url or self._notify_url): + raise Exception('notify_url is not assigned.') + params.update({'notify_url': notify_url or self._notify_url}) + path = '/v3/marketing/busifavor/callbacks' + return self._core.request(path, method=RequestType.POST, data=params) + + +def marketing_busifavor_callback_query(self, mchid=None): + """查询商家券事件通知地址 + :param mchid: 商户号,不填默认使用初始化的mchid。示例值:'10000098' + """ + path = '/v3/marketing/busifavor/callbacks?mchid=%s' % (mchid or self._mchid) + return self._core.request(path) + + +def marketing_busifavor_coupon_associate(self, stock_id, coupon_code, out_trade_no, out_request_no): + """关联订单信息 + :param stock_id: 批次号。示例值:1212 + :param coupon_code: 券code,券的唯一标识。示例值:'sxxe34343434' + :param out_trade_no: 关联的商户订单号,微信支付下单时的商户订单号,欲与该商家券关联的微信支付。示例值:'MCH_102233445' + :param out_request_no: 商户请求单号,示例值:'1002600620019090123143254435' + """ + params = {} + if stock_id: + params.update({'stock_id': stock_id}) + else: + raise Exception('stock_id is not assigned.') + if coupon_code: + params.update({'coupon_code': coupon_code}) + else: + raise Exception('coupon_code is not assigned.') + if out_trade_no: + params.update({'out_trade_no': out_trade_no}) + else: + raise Exception('out_trade_no is not assigned.') + if out_request_no: + params.update({'out_request_no': out_request_no}) + else: + raise Exception('out_request_no is not assigned.') + path = '/v3/marketing/busifavor/coupons/associate' + return self._core.request(path, method=RequestType.POST, data=params) + + +def marketing_busifavor_coupon_disassociate(self, stock_id, coupon_code, out_trade_no, out_request_no): + """取消关联订单信息 + :param stock_id: 批次号。示例值:1212 + :param coupon_code: 券code,券的唯一标识。示例值:'sxxe34343434' + :param out_trade_no: 关联的商户订单号,微信支付下单时的商户订单号,欲与该商家券关联的微信支付。示例值:'MCH_102233445' + :param out_request_no: 商户请求单号,示例值:'1002600620019090123143254435' + """ + params = {} + if stock_id: + params.update({'stock_id': stock_id}) + else: + raise Exception('stock_id is not assigned.') + if coupon_code: + params.update({'coupon_code': coupon_code}) + else: + raise Exception('coupon_code is not assigned.') + if out_trade_no: + params.update({'out_trade_no': out_trade_no}) + else: + raise Exception('out_trade_no is not assigned.') + if out_request_no: + params.update({'out_request_no': out_request_no}) + else: + raise Exception('out_request_no is not assigned.') + path = '/v3/marketing/busifavor/coupons/disassociate' + return self._core.request(path, method=RequestType.POST, data=params) + + +def marketing_busifavor_stock_budget(self, + stock_id, + modify_budget_request_no, + target_max_coupons=None, + target_max_coupons_by_day=None, + current_max_coupons=None, + current_max_coupons_by_day=None): + """修改批次预算 + :param stock_id: 批次号。示例值:1212 + :param modify_budget_request_no: 修改预算请求单据号,示例值:'1002600620019090123143254436' + :param target_max_coupons: 目标批次最大发放个数。示例值:3000 + :param target_max_coupons_by_day: 目标单天发放上限个数。示例值:500 + :param current_max_coupons: 当前批次最大发放个数。示例值:500 + :param current_max_coupons_by_day: 当前单天发放上限个数。示例值:300 + """ + params = {} + if stock_id: + path = '/v3/marketing/busifavor/stocks/%s/budget' % stock_id + else: + raise Exception('stock_id is not assigned.') + if modify_budget_request_no: + params.update({'modify_budget_request_no': modify_budget_request_no}) + else: + raise Exception('modify_budget_request_no is not assigned.') + if target_max_coupons: + params.update({'target_max_coupons': target_max_coupons}) + elif target_max_coupons_by_day: + params.update({'target_max_coupons_by_day': target_max_coupons_by_day}) + else: + raise Exception('target_max_coupons or target_max_coupons_by_day is not assigned.') + if current_max_coupons: + params.update({'current_max_coupons': current_max_coupons}) + if current_max_coupons_by_day: + params.update({'current_max_coupons_by_day': current_max_coupons_by_day}) + return self._core.request(path, method=RequestType.PATCH, data=params) + + +def marketing_busifavor_stock_modify(self, + stock_id, + out_request_no, + custom_entrance=None, + comment=None, + goods_name=None, + display_pattern_info=None, + coupon_use_rule=None, + stock_send_rule=None, + notify_config=None): + """修改商家券基本信息 + :param stock_id: 批次号。示例值:1212 + :param out_request_no: 商户请求单号,示例值:'1002600620019090123143254435' + :param custom_entrance: 自定义入口。示例值:{'hall_id':'234567'} + :param comment: 批次备注,字数上限为20个。示例值:'活动使用' + :param goods_name: 适用商品范围。示例值:'xxx商品使用' + :param display_pattern_info: 样式信息。示例值:{'description':'xxx门店可用'} + :param coupon_use_rule: 核销规则。示例值:{'use_method':'OFF_LINE'} + :param stock_send_rule: 发放规则。示例值:{'prevent_api_abuse':False} + :param notify_config: 事件通知配置。示例值:{'notify_appid':'wx23232232323'} + """ + if stock_id: + path = '/v3/marketing/busifavor/stocks/%s' % stock_id + else: + raise Exception('stock_id is not assigned.') + params = {} + if out_request_no: + params.update({'out_request_no': out_request_no}) + else: + raise Exception('out_request_no is not assigned.') + if custom_entrance: + params.update({'custom_entrance': custom_entrance}) + if comment: + params.update({'comment': comment}) + if goods_name: + params.update({'goods_name': goods_name}) + if display_pattern_info: + params.update({'display_pattern_info': display_pattern_info}) + if coupon_use_rule: + params.update({'coupon_use_rule': coupon_use_rule}) + if stock_send_rule: + params.update({'stock_send_rule': stock_send_rule}) + if notify_config: + params.update({'notify_config': notify_config}) + return self._core.request(path, method=RequestType.PATCH, data=params) + + +def marketing_busifavor_coupon_return(self, coupon_code, stock_id, return_request_no): + """申请退券 + :param coupon_code: 券code,券的唯一标识。示例值:'sxxe34343434' + :param stock_id: 批次号。示例值:1212 + :param return_request_no: 退券请求单据号。示例值:'1002600620019090123143254436' + """ + params = {} + if coupon_code: + params.update({'coupon_code': coupon_code}) + else: + raise Exception('coupon_code is not assigned.') + if stock_id: + params.update({'stock_id': stock_id}) + else: + raise Exception('stock_id is not assigned.') + if return_request_no: + params.update({'return_request_no': return_request_no}) + else: + raise Exception('return_request_no is not assigned.') + path = '/v3/marketing/busifavor/coupons/return' + return self._core.request(path, method=RequestType.POST, data=params) + + +def marketing_busifavor_coupon_deactivate(self, coupon_code, stock_id, deactivate_request_no, deactivate_reason=None): + """使券失效 + :param coupon_code: 券code,券的唯一标识。示例值:'sxxe34343434' + :param stock_id: 批次号。示例值:1212 + :param deactivate_request_no: 失效请求单据号。示例值:'1002600620019090123143254436' + :param deactivate_reason: 失效原因。示例值:'此券使用时间设置错误' + """ + params = {} + if coupon_code: + params.update({'coupon_code': coupon_code}) + else: + raise Exception('coupon_code is not assigned.') + if stock_id: + params.update({'stock_id': stock_id}) + else: + raise Exception('stock_id is not assigned.') + if deactivate_request_no: + params.update({'deactivate_request_no': deactivate_request_no}) + else: + raise Exception('deactivate_request_no is not assigned.') + if deactivate_reason: + params.update({'deactivate_reason': deactivate_reason}) + path = '/v3/marketing/busifavor/coupons/deactivate' + return self._core.request(path, method=RequestType.POST, data=params) + + +def marketing_busifavor_subsidy_pay(self, + stock_id, + coupon_code, + transaction_id, + payer_merchant, + payee_merchant, + amount, + description, + out_subsidy_no): + """营销补差付款 + :param stock_id: 批次号。示例值:1212 + :param coupon_code: 券code,券的唯一标识。示例值:'sxxe34343434' + :param transaction_id: 微信支付订单号。示例值:'4200000913202101152566792388' + :param payer_merchant: 营销补差扣款商户号。示例值:'1900000001' + :param payee_merchant: 营销补差入账商户号。示例值:'1900000002' + :param amount: 补差付款金额。示例值:100 + :param description: 补差付款描述。示例值:'20210115DESCRIPTION' + :param out_subsidy_no: 业务请求唯一单号。示例值:'subsidy-abcd-12345678' + """ + params = {} + if coupon_code: + params.update({'coupon_code': coupon_code}) + else: + raise Exception('coupon_code is not assigned.') + if stock_id: + params.update({'stock_id': stock_id}) + else: + raise Exception('stock_id is not assigned.') + if transaction_id: + params.update({'transaction_id': transaction_id}) + else: + raise Exception('transaction_id is not assigned.') + if payer_merchant: + params.update({'payer_merchant': payer_merchant}) + else: + raise Exception('payer_merchant is not assigned.') + if payee_merchant: + params.update({'payee_merchant': payee_merchant}) + else: + raise Exception('payee_merchant is not assigned.') + if amount: + params.update({'amount': amount}) + else: + raise Exception('amount is not assigned.') + if description: + params.update({'description': description}) + else: + raise Exception('description is not assigned.') + if out_subsidy_no: + params.update({'out_subsidy_no': out_subsidy_no}) + else: + raise Exception('out_subsidy_no is not assigned.') + path = '/v3/marketing/busifavor/subsidy/pay-receipts' + return self._core.request(path, method=RequestType.POST, data=params) + + +def marketing_busifavor_subsidy_query(self, subsidy_receipt_id): + """查询营销补差付款单详情 + :param subsidy_receipt_id: 补差付款单号。示例值:'1120200119165100000000000001' + """ + if subsidy_receipt_id: + path = '/v3/marketing/busifavor/subsidy/pay-receipts/%s' % subsidy_receipt_id + else: + raise Exception('subsidy_receipt_id is not assigned.') + return self._core.request(path) + + +def industry_coupon_token(self, open_id, coupon_list=[]): + """出行券切卡组件预下单 + https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter9_9_1.shtml + :param open_id: 用户在商户AppID下的唯一标识,该用户为后续拉起切卡组件的用户。示例值:'obLatjrR8kUDlj4-nofQsPAJAAFI' + :param coupon_list: 用户最近领取的出行券列表。示例值:[{"coupon_id": "11004999626", "stock_id": 16474341}] + """ + params = {} + if open_id: + params.update({'open_id': open_id}) + else: + raise Exception('open_id is not assigned.') + if coupon_list: + params.update({'coupon_list': coupon_list}) + else: + raise Exception('coupon_list is not assigned.') + path = '/v3/industry-coupon/tokens' + return self._core.request(path, method=RequestType.POST, data=params) + + +def bank_package_file(self, package_id, bank_type, filepath): + """导入定向用户协议号 + https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter9_8_1.shtml + :package_id: 号码包唯一标识符。可在微信支付商户平台创建号码包后获得。示例值:'8473295' + :filepath: 电子发票文件路径,只支持txt和csv两种格式,示例值:'./active_user.csv' + """ + if not (filepath and os.path.exists(filepath) and os.path.isfile(filepath)): + raise Exception('filepath is not assigned or not exists') + with open(filepath, mode='rb') as f: + content = f.read() + filename = os.path.basename(filepath) + filetype = os.path.splitext(filename)[-1][1:].upper() + mimes = { + 'TXT': ' text/plain', + 'CSV': 'text/csv' + } + if filetype not in mimes: + raise Exception(f'wechatpayv3 does not support this file type: {filetype}') + if not package_id or bank_type: + raise Exception('package_id or bank_type is not assigned.') + params = {} + params.update({'meta': '{"bank_type":"%s", "filename":"%s", "sha256":"%s"}' % (bank_type, filename, sha256(content))}) + files = [('file', (filename, content, mimes[filetype]))] + path = '/v3/marketing/bank/packages/%s/tasks' % package_id + return self._core.request(path, method=RequestType.POST, data=params, sign_data=params.get('meta'), files=files) diff --git a/wechatpayv3/mchtransfer.py b/wechatpayv3/mchtransfer.py new file mode 100644 index 0000000..4f2f965 --- /dev/null +++ b/wechatpayv3/mchtransfer.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType + +def mch_transfer_bills(self, out_bill_no, transfer_scene_id, openid, transfer_amount, transfer_remark, user_name=None, user_recv_perception=None, transfer_scene_report_infos=[], appid=None, notify_url=None): + """发起转账 + :param out_bill_no: 商户单号,商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一,示例值:'plfk2020042013' + :param transfer_scene_id: 转账场景ID,示例值:'1001' + :param openid: 收款用户OpenID,商户AppID下,某用户的OpenID,示例值:'o-MYE42l80oelYMDE34nYD456Xoy' + :param transfer_amount: 转账金额,单位为“分”,示例值: 1000 + :param transfer_remark: 转账备注,用户收款时可见该备注信息,最多允许32个字符,示例值:'2020年4月报销' + :param user_name: 收款用户姓名,转账金额 >= 2,000元时,该笔明细必须填写。若商户传入收款用户姓名,微信支付会校验收款用户与输入姓名是否一致,并提供电子回单,示例值:'张三' + :param user_recv_perception: 用户收款时感知到的收款原因,将根据转账场景自动展示默认内容。如有其他展示需求,可在本字段传入。示例值: '现金奖励' + :param transfer_scene_report_infos: 转账场景报备信息,info_type的值必需按文档指示传入,示例值: [{'info_type':'活动名称', 'info_content':'新会员有礼'}, {'info_type':'奖励说明', 'info_content':'注册会员抽奖'}] + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param notify_url: 通知地址,异步接收微信支付结果通知的回调地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php' + """ + params={} + if out_bill_no: + params.update({'out_bill_no':out_bill_no}) + else: + raise Exception('out_batch_no is not assigned') + if transfer_scene_id: + params.update({'transfer_scene_id':transfer_scene_id}) + else: + raise Exception('transfer_scene_id is not assigned') + if openid: + params.update({'openid':openid}) + else: + raise Exception('openid is not assigned') + if transfer_amount: + params.update({'transfer_amount':transfer_amount}) + else: + raise Exception('transfer_amount is not assigned') + if transfer_remark: + params.update({'transfer_remark':transfer_remark}) + else: + raise Exception('transfer_remark is not assigned') + cipher_data = False + if user_name and transfer_amount >= 30: + params.update({'user_name':self._core.encrypt(user_name)}) + cipher_data = True + if transfer_amount >= 200000 and not user_name: + raise Exception('user_name is not assigned') + if user_recv_perception: + params.update({'user_recv_perception':user_recv_perception}) + if transfer_scene_report_infos: + params.update({'transfer_scene_report_infos':transfer_scene_report_infos}) + params.update({'appid': appid or self._appid}) + params.update({'notify_url': notify_url or self._notify_url}) + path = '/v3/fund-app/mch-transfer/transfer-bills' + return self._core.request(path, method=RequestType.POST, data=params, cipher_data=cipher_data) + +def mch_transfer_bills_cancel(self, out_bill_no): + """撤销转账 + :param out_bill_no: 商户单号,商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一,示例值:'plfk2020042013' + """ + if out_bill_no: + path = f'/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}/cancel' + else: + raise Exception('out_bill_no is not assigned') + return self._core.request(path, method=RequestType.POST) + +def mch_transfer_bills_query(self, out_bill_no=None, transfer_bill_no=None): + """查询转账单 + :param out_bill_no: 商户单号,商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一,示例值:'plfk2020042013' + :param transfer_bill_no: 微信转账单号,微信商家转账系统返回的唯一标识,示例值: '1330000071100999991182020050700019480001' + """ + if not (out_bill_no or transfer_bill_no): + raise Exception('out_bill_no or transfer_bill_no is not assigned') + if out_bill_no: + path = f'/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}' + else: + path = f'/v3/fund-app/mch-transfer/transfer-bills/transfer-bill-no/{transfer_bill_no}' + return self._core.request(path) + +def mch_transfer_elecsign(self, out_bill_no=None, transfer_bill_no=None): + """申请电子回单 + :param out_bill_no: 商户单号,商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一,示例值:'plfk2020042013' + :param transfer_bill_no: 微信转账单号,微信商家转账系统返回的唯一标识,示例值: '1330000071100999991182020050700019480001' + """ + if not (out_bill_no or transfer_bill_no): + raise Exception('out_bill_no or transfer_bill_no is not assigned') + params = {} + if out_bill_no: + params.update({'out_bill_no':out_bill_no}) + path = '/v3/fund-app/mch-transfer/elecsign/out-bill-no' + else: + params.update({'transfer_bill_no':transfer_bill_no}) + path = '/v3/fund-app/mch-transfer/elecsign/transfer-bill-no' + return self._core.request(path, method=RequestType.POST, data=params) + +def mch_transfer_elecsign_query(self, out_bill_no=None, transfer_bill_no=None): + """查询电子回单 + :param out_bill_no: 商户单号,商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一,示例值:'plfk2020042013' + :param transfer_bill_no: 微信转账单号,微信商家转账系统返回的唯一标识,示例值: '1330000071100999991182020050700019480001' + """ + if not (out_bill_no or transfer_bill_no): + raise Exception('out_bill_no or transfer_bill_no is not assigned') + if out_bill_no: + path = f'/v3/fund-app/mch-transfer/elecsign/out-bill-no/{out_bill_no}' + else: + path = f'/v3/fund-app/mch-transfer/elecsign/transfer-bill-no/{transfer_bill_no}' + return self._core.request(path) diff --git a/wechatpayv3/media.py b/wechatpayv3/media.py new file mode 100644 index 0000000..fe35e66 --- /dev/null +++ b/wechatpayv3/media.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +import os.path + +from .type import RequestType +from .utils import sha256 + + +def _media_upload(self, filepath, filename, path): + if not (filepath and os.path.exists(filepath) and os.path.isfile(filepath) and path): + raise Exception('filepath is not assigned or not exists') + with open(filepath, mode='rb') as f: + content = f.read() + if not filename: + filename = os.path.basename(filepath) + params = {} + params.update({'meta': '{"filename":"%s","sha256":"%s"}' % (filename, sha256(content))}) + mimes = { + '.bmp': 'image/bmp', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.avi': 'video/x-msvideo', + '.wmv': 'video/x-ms-wmv', + '.mpeg': 'video/mpeg', + '.mp4': 'video/mp4', + '.mov': 'video/quicktime', + '.mkv': 'video/x-matroska', + '.flv': 'video/x-flv', + '.f4v': 'video/x-f4v', + '.m4v': 'video/x-m4v', + '.rmvb': 'application/vnd.rn-realmedia-vbr' + } + media_type = os.path.splitext(filename)[-1] + if media_type not in mimes: + raise Exception(f'wechatpayv3 does not support this media type: {media_type}') + files = [('file', (filename, content, mimes[media_type]))] + return self._core.request(path, method=RequestType.POST, data=params, sign_data=params.get('meta'), files=files) + + +def image_upload(self, filepath, filename=None): + """图片上传 + :param filepath: 图片文件路径 + :param filename: 文件名称,未指定则从filepath参数中截取 + """ + return _media_upload(self, filepath, filename, path='/v3/merchant/media/upload') + + +def video_upload(self, filepath, filename=None): + """视频上传 + :param filepath: 视频文件路径 + :param filename: 文件名称,未指定则从filepath参数中截取 + """ + return _media_upload(self, filepath, filename, path='/v3/merchant/media/video_upload') diff --git a/wechatpayv3/merchantrisk.py b/wechatpayv3/merchantrisk.py new file mode 100644 index 0000000..4d8032c --- /dev/null +++ b/wechatpayv3/merchantrisk.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType + + +def merchantrisk_callback_create(self, notify_url=None): + """创建商户违规通知回调地址 + :param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php' + """ + params = {} + if notify_url: + params.update({'notify_url': notify_url}) + path = '/v3/merchant-risk-manage/violation-notifications' + return self._core.request(path, method=RequestType.POST, data=params) + + +def merchantrisk_callback_query(self): + """查询商户违规通知回调地址 + """ + path = '/v3/merchant-risk-manage/violation-notifications' + return self._core.request(path) + + +def merchantrisk_callback_update(self, notify_url=None): + """修改商户违规通知回调地址 + :param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php' + """ + params = {} + if notify_url: + params.update({'notify_url': notify_url}) + path = '/v3/merchant-risk-manage/violation-notifications' + return self._core.request(path, method=RequestType.PUT, data=params) + + +def merchantrisk_callback_delete(self): + """查询商户违规通知回调地址 + """ + path = '/v3/merchant-risk-manage/violation-notifications' + return self._core.request(path, method=RequestType.DELETE) diff --git a/wechatpayv3/parking.py b/wechatpayv3/parking.py new file mode 100644 index 0000000..6f4f3be --- /dev/null +++ b/wechatpayv3/parking.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType + + +def parking_service_find(self, plate_number, plate_color, openid, sub_mchid=None): + """查询车牌服务开通信息 + :param plate_number: 车牌号,示例值:'粤B888888' + :param plate_color: 车牌颜色,车牌颜色,枚举值:BLUE:蓝色,GREEN:绿色,YELLOW:黄色,BLACK:黑色,WHITE:白色,LIMEGREEN:黄绿色 + :param openid: 用户标识,示例值:'oUpF8uMuAJOM2pxb1Q' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + path = '/v3/vehicle/parking/services/find?appid=%s' % self._appid + if plate_number: + path = '%s&plate_number=%s' % (path, plate_number) + else: + raise Exception('plate_number is not assigned.') + if plate_color: + path = '%s&plate_color=%s' % (path, plate_color) + else: + raise Exception('plate_color is not assigned.') + if openid: + path = '%s&openid=%s' % (path, openid) + else: + raise Exception('openid is not assigned.') + if self._partner_mode: + if sub_mchid: + path = '%s&sub_mchid=%s' % (path, sub_mchid) + else: + raise Exception('sub_mchid is not assigned.') + return self._core.request(path) + + +def parking_enter(self, out_parking_no, plate_number, plate_color, start_time, parking_name, free_duration, notify_url=None, sub_mchid=None): + """创建停车入场 + :param out_parking_no: 商户入场id,商户侧入场标识id,在同一个商户号下唯一,示例值:'1231243' + :param plate_number: 车牌号,示例值:'粤B888888' + :param plate_color: 车牌颜色,车牌颜色,枚举值:BLUE:蓝色,GREEN:绿色,YELLOW:黄色,BLACK:黑色,WHITE:白色,LIMEGREEN:黄绿色 + :param notify_url: 回调通知url,接受入场状态变更回调通知的url,只接受https,示例值:https://yoursite.com/wxpay.html + :param start_time: 入场时间,示例值:'2017-08-26T10:43:39+08:00' + :param parking_name: 停车场名称,示例值:'欢乐海岸停车场' + :param free_duration: 免费时长,单位为秒,示例值:3600 + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if out_parking_no: + params.update({'out_parking_no': out_parking_no}) + else: + raise Exception('out_parking_no is not assigned') + if plate_number: + params.update({'plate_number': plate_number}) + else: + raise Exception('plate_number is not assigned') + if plate_color: + params.update({'plate_color': plate_color}) + else: + raise Exception('plate_color is not assigned') + if not (notify_url or self._notify_url): + raise Exception('notify_url is not assigned.') + params.update({'notify_url': notify_url or self._notify_url}) + if start_time: + params.update({'start_time': start_time}) + else: + raise Exception('start_time is not assigned') + if parking_name: + params.update({'parking_name': parking_name}) + else: + raise Exception('parking_name is not assigned') + if free_duration: + params.update({'free_duration': free_duration}) + else: + raise Exception('free_duration is not assigned') + if self._partner_mode: + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + path = '/v3/vehicle/parking/parkings' + return self._core.request(path, method=RequestType.POST, data=params) + + +def parking_order(self, description, out_trade_no, total, parking_id, plate_number, plate_color, start_time, + end_time, parking_name, charging_duration, device_id, trade_scene='PARKING', profit_sharing='N', + currency='CNY', attach=None, goods_tag=None, notify_url=None, appid=None, sub_appid=None, sub_mchid=None): + """停车扣费受理 + :param description: 服务描述,商户自定义字段,用于交易账单中对扣费服务的描述。示例值:'停车场扣费' + :param out_trade_no: 商户订单号,商户系统内部订单号,只能是数字、大小写字母,且在同一个商户号下唯一,示例值:'20150806125346' + :param notify_url: 回调通知url,只接受https,示例值:'https://yoursite.com/wxpay.html' + :param total: 订单总金额,单位为分,只能为整数,示例值:888 + :param parking_id: 停车入场id,通过入场通知接口获取的入场id,示例值:'5K8264ILTKCH16CQ250' + :param plate_number: 车牌号,仅包括省份+车牌,不包括特殊字符。示例值:'粤B888888' + :param plate_color: 车牌颜色,枚举值:BLUE:蓝色,GREEN:绿色,YELLOW:黄色,BLACK:黑色,WHITE:白色,LIMEGREEN:黄绿色,示例值:BLUE + :param start_time: 入场时间,示例值:'2017-08-26T10:43:39+08:00' + :param end_time: 出场时间,示例值:'2017-08-26T10:43:39+08:00' + :param parking_name: 停车场名称,示例值:'欢乐海岸停车场' + :param charging_duration: 计费时长,单位为秒,示例值:3600 + :param device_id: 停车场设备id,示例值:'12313' + :param trade_scene: 交易场景值,目前支持'PARKING':车场停车场景 + :param profit_sharing: 分账标识,枚举值:'Y':是,需要分账,'N':否,不分账,字母要求大写,不传默认不分账。 + :param currency: 货币类型,目前只支持人民币:'CNY' + :param attach: 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用,示例值:'深圳分店' + :param goods_tag: 订单优惠标记,代金券或立减优惠功能的参数,示例值:WXG + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param sub_appid: (服务商模式)子商户应用ID,示例值:'wxd678efh567hg6999' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + amount = {} + parking_info = {} + params.update({'appid': appid or self._appid}) + if description: + params.update({'description': description}) + else: + raise Exception('description is not assigned') + if out_trade_no: + params.update({'out_trade_no': out_trade_no}) + else: + raise Exception('out_trade_no is not assigned') + params.update({'notify_url': notify_url or self._notify_url}) + if total: + amount.update({'total': total}) + else: + raise Exception('total is not assigned') + if parking_id: + parking_info.update({'parking_id': parking_id}) + else: + raise Exception('parking_id is not assigned') + if plate_number: + parking_info.update({'plate_number': plate_number}) + else: + raise Exception('plate_number is not assigned') + if plate_color: + parking_info.update({'plate_color': plate_color}) + else: + raise Exception('plate_color is not assigned') + if start_time: + parking_info.update({'start_time': start_time}) + else: + raise Exception('start_time is not assigned') + if end_time: + parking_info.update({'end_time': end_time}) + else: + raise Exception('end_time is not assigned') + if parking_name: + parking_info.update({'parking_name': parking_name}) + else: + raise Exception('parking_name is not assigned') + if charging_duration: + parking_info.update({'charging_duration': charging_duration}) + else: + raise Exception('charging_duration is not assigned') + if device_id: + parking_info.update({'device_id': device_id}) + else: + raise Exception('device_id is not assigned') + if trade_scene: + params.update({'trade_scene': trade_scene}) + else: + raise Exception('trade_scene is not assigned') + if profit_sharing: + params.update({'profit_sharing': profit_sharing}) + else: + raise Exception('profit_sharing is not assigned') + if currency: + amount.update({'currency': currency}) + else: + raise Exception('currency is not assigned') + if attach: + params.update({'attach': attach}) + if goods_tag: + params.update({'goods_tag': goods_tag}) + params.update({'amount': amount}) + params.update({'parking_info': parking_info}) + if self._partner_mode: + if sub_appid: + params.update({'sub_appid': sub_appid}) + else: + raise Exception('sub_appid is not assigned.') + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + path = '/v3/vehicle/transactions/parking' + return self._core.request(path, method=RequestType.POST, data=params) + + +def parking_order_query(self, out_trade_no, sub_mchid=None): + """停车扣费订单查询 + :param out_trade_no: 商户订单号,商户系统内部订单号,只能是数字、大小写字母,且在同一个商户号下唯一,示例值:'20150806125346' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + if out_trade_no: + path = '/v3/vehicle/transactions/out-trade-no/%s' % out_trade_no + else: + raise Exception('out_trade_no is not assigned') + if self._partner_mode: + if sub_mchid: + path = '%s?sub_mchid=%s' % (path, sub_mchid) + else: + raise Exception('sub_mchid is not assigned.') + return self._core.request(path) diff --git a/wechatpayv3/payscore.py b/wechatpayv3/payscore.py new file mode 100644 index 0000000..21fbffd --- /dev/null +++ b/wechatpayv3/payscore.py @@ -0,0 +1,389 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType +from .transaction import query_refund, refund + + +def payscore_direct_complete(self, out_order_no, openid, service_id, service_introduction, post_payments, + time_range, total_amount, post_discounts=None, location=None, + profit_sharing=False, goods_tag=None, attach=None, notify_url=None, appid=None): + """创单结单合并 + :param out_order_no: 商户服务订单号,商户系统内部服务订单号(不是交易单号),要求此参数只能由数字、大小写字母_-|*组成,且在同一个商户号下唯一。示例值:'1234323JKHDFE1243252' + :param openid: 用户标识,微信用户在商户对应appid下的唯一标识。示例值:'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o' + :param service_id: 服务ID。示例值:'500001' + :param service_introduction: 服务信息,用于介绍本订单所提供的服务 ,当参数长度超过20个字符时,报错处理。示例值:'某某酒店' + :param post_payments: 付费项目列表,最多包含100条付费项目。 + :param time_range: 服务时间范围。 + :param total_amount: 总金额,总金额 =(完结付费项目1…+完结付费项目n)-(完结商户优惠项目1…+完结商户优惠项目n)。示例值:50000 + :param post_discounts: 商户优惠,付费商户优惠列表,最多包含30条商户优惠。 + :param location: 服务位置,如果传入,用户侧则显示此参数。 + :param profit_sharing: 微信支付服务分账标记,默认为false,枚举值:False:不分账,True:分账。示例值:False + :param goods_tag: 订单优惠标记。示例值:'goods_tag1' + :param attach: 商户数据包。商户数据包可存放本订单所需信息,需要先urlencode后传入。当商户数据包总长度超出256字符时,报错处理。示例值:'Easdfowealsdkjfnlaksjdlfkwqoi&wl3l2sald' + :param notify_url: 商户回调地址,商户接收扣款成功回调通知的地址,服务需要收款时此参数必填;服务无需收款时此参数不填。示例值:'https://api.test.com' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if not (out_order_no and openid and service_id and service_introduction and post_payments and time_range and total_amount): + raise Exception('ut_order_no or openid or service_id or service_introduction or post_payments or time_range or total_amount is not assigned.') + params.update({'appid': appid or self._appid}) + params.update({'out_order_no': out_order_no}) + params.update({'openid': openid}) + params.update({'service_id': service_id}) + params.update({'service_introduction': service_introduction}) + params.update({'post_payments': post_payments}) + params.update({'time_range': time_range}) + params.update({'total_amount': total_amount}) + if post_discounts: + params.update({'post_discounts': post_discounts}) + if location: + params.update({'location': location}) + if profit_sharing: + params.update({'profit_sharing': profit_sharing}) + if goods_tag: + params.update({'goods_tag': goods_tag}) + if attach: + params.update({'attach': attach}) + payment = False + for item in post_payments: + if item.get('amount') > 0: + payment = True + break + if payment: + if not (notify_url or self._notify_url): + raise Exception('notify_url is not assigned.') + params.update({'notify_url': notify_url or self._notify_url}) + path = '/payscore/serviceorder/direct-complete' + return self._core.request(path, method=RequestType.POST, data=params) + + +def payscore_permission(self, service_id, authorization_code, notify_url=None, appid=None): + """商户预授权 + :param service_id: 服务ID。示例值:'500001' + :param authorization_code: 授权协议号,户系统内部授权协议号,要求此参数只能由数字、大小写字母_-*组成,且在同一个商户号下唯一。示例值:'1234323JKHDFE1243252' + :param notify_url: 通知地址,商户接收授权回调通知的地址。示例值:'http://www.qq.com' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if not (service_id and authorization_code): + raise Exception('service_id or authorization_code is not assigned.') + params.update({'appid': appid or self._appid}) + params.update({'service_id': service_id}) + params.update({'authorization_code': authorization_code}) + params.update({'notify_url': notify_url or self._notify_url}) + path = '/v3/payscore/permissions' + return self._core.request(path, method=RequestType.POST, data=params) + + +def payscore_permission_query(self, service_id, authorization_code=None, openid=None): + """查询用户授权记录(授权协议号或openid) + :param service_id: 服务ID。示例值:'500001' + :param authorization_code: 授权协议号,户系统内部授权协议号,要求此参数只能由数字、大小写字母_-*组成,且在同一个商户号下唯一。示例值:'1234323JKHDFE1243252' + :param openid: 用户标识,微信用户在商户对应appid下的唯一标识。示例值:'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o' + """ + if not service_id: + raise Exception('service_id is not assigned.') + if authorization_code: + path = '/v3/payscore/permissions/authorization-code/%s?service_id=%s' % (authorization_code, service_id) + elif openid: + path = '/v3/payscore/permissions/openid/%s?appid=%s&service_id=%s' % (openid, self._appid, service_id) + else: + raise Exception('authorization_code or openid is not assigned.') + return self._core.request(path) + + +def payscore_permission_terminate(self, service_id, reason, authorization_code=None, openid=None, appid=None): + """解除用户授权记录(授权协议号或openid) + :param service_id: 服务ID。示例值:'500001' + :param reason: 撤销原因,解除授权原因。示例值:'撤销原因' + :param authorization_code: 授权协议号,户系统内部授权协议号,要求此参数只能由数字、大小写字母_-*组成,且在同一个商户号下唯一。示例值:'1234323JKHDFE1243252' + :param openid: 用户标识,微信用户在商户对应appid下的唯一标识。示例值:'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if not (service_id and reason): + raise Exception('service_id or reason is not assigned.') + params.update({'service_id': service_id}) + params.update({'reason': reason}) + if authorization_code: + path = 'v3/payscore/permissions/authorization-code/%s/terminate' % authorization_code + elif openid: + params.update({'appid': appid or self._appid}) + path = '/v3/payscore/permissions/openid/%s/terminate' % openid + else: + raise Exception('authorization_code or openid is not assigned.') + return self._core.request(path, method=RequestType.POST, data=params) + + +def payscore_create(self, out_order_no, service_id, service_introduction, time_range, + risk_fund, attach=None, openid=None, post_payments=None, post_discounts=None, + location=None, need_user_confirm=True, notify_url=None, appid=None): + """创建支付分订单 + :param out_order_no: 商户服务订单号,商户系统内部服务订单号(不是交易单号),要求此参数只能由数字、大小写字母_-|*组成,且在同一个商户号下唯一。示例值:'1234323JKHDFE1243252' + :param service_id: 服务ID,该服务ID有本接口对应产品的权限。示例值:'500001' + :param service_introduction: 服务信息,用于介绍本订单所提供的服务 ,当参数长度超过20个字符时,报错处理。示例值:'某某酒店' + :param time_range: 服务时间范围。 + :param risk_fund: 订单风险金。 + :param attach: 商户数据包,商户数据包可存放本订单所需信息,需要先urlencode后传入。当商户数据包总长度超出256字符时,报错处理。示例值:'Easdfowealsdkjfnlaksjdlfkwqoi&wl3l2sald' + :param openid: 用户标识,微信用户在商户对应appid下的唯一标识。免确认订单:必填,需确认订单:不填。示例值:'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o' + :param post_payments: 后付费项目,后付费项目列表,最多包含100条付费项目。如果传入,用户侧则显示此参数。 + :param post_discounts: 后付费商户优惠,后付费商户优惠列表,最多包含30条商户优惠。如果传入,用户侧则显示此参数。 + :param location: 服务位置信息,如果传入,用户侧则显示此参数。 + :param need_user_confirm: 是否需要用户确认,枚举值:False:免确认订单,True:需确认订单,默认值True。示例值:True + :param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if out_order_no: + params.update({'out_order_no': out_order_no}) + else: + raise Exception('out_order_no is not assigned.') + params.update({'appid': appid or self._appid}) + if service_id: + params.update({'service_id': service_id}) + else: + raise Exception('service_id is not assigned.') + if service_introduction: + params.update({'service_introduction': service_introduction}) + else: + raise Exception('service_introduction is not assigned.') + if time_range: + params.update({'time_range': time_range}) + else: + raise Exception('time_range is not assigned.') + if risk_fund: + params.update({'risk_fund': risk_fund}) + else: + raise Exception('risk_fund is not assigned.') + if attach: + params.update({'attach': attach}) + if post_payments: + params.update({'post_payments': post_payments}) + if post_discounts: + params.update({'post_discounts': post_discounts}) + if location: + params.update({'location': location}) + params.update({'need_user_confirm': need_user_confirm}) + if not need_user_confirm: + if openid: + params.update({'openid': openid}) + else: + raise Exception('openid is not assigned.') + params.update({'notify_url': notify_url or self._notify_url}) + path = '/v3/payscore/serviceorder' + return self._core.request(path, method=RequestType.POST, data=params) + + +def payscore_query(self, service_id, out_order_no=None, query_id=None): + """查询支付分订单 + :param service_id: 服务ID,该服务ID有本接口对应产品的权限。示例值:'500001' + :param out_order_no: 商户服务订单号,商户系统内部服务订单号(不是交易单号),要求此参数只能由数字、大小写字母_-|*组成,且在同一个商户号下唯一。示例值:'1234323JKHDFE1243252' + :param query_id: 回跳查询ID,微信侧回跳到商户前端时用于查单的单据查询id。商户单号与回跳查询id必填其中一个。不允许都填写或都不填写。示例值:'15646546545165651651' + """ + if service_id: + path = '/v3/payscore/serviceorder?service_id=%s&appid=%s' % (service_id, self._appid) + else: + raise Exception('service_id is not assigned.') + if out_order_no: + path = '%s&out_order_no=%s' % (path, out_order_no) + elif query_id: + path = '%s&query_id=%s' % (path, query_id) + else: + raise Exception('out_order_no or query_id is not assigned.') + return self._core.request(path) + + +def payscore_cancel(self, out_order_no, service_id, reason, appid=None): + """取消支付分订单 + :param out_order_no: 商户服务订单号,商户系统内部服务订单号(不是交易单号),要求此参数只能由数字、大小写字母_-|*组成,且在同一个商户号下唯一。示例值:'1234323JKHDFE1243252' + :param service_id: 服务ID,该服务ID有本接口对应产品的权限。示例值:'500001' + :param query_id: 回跳查询ID,微信侧回跳到商户前端时用于查单的单据查询id。商户单号与回跳查询id必填其中一个。不允许都填写或都不填写。示例值:'15646546545165651651' + :param reason: 取消原因,最多30个字符,每个汉字/数字/英语都按1个字符计算超过长度报错处理。注:重录时需保证参数完全一致,包括取消原因。示例值:'用户投诉' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if out_order_no: + path = '/v3/payscore/serviceorder/%s/cancel' % out_order_no + else: + raise Exception('out_order_no is not assigned.') + if service_id: + params.update({'service_id': service_id}) + else: + raise Exception('service_id is not assigned.') + if reason: + params.update({'reason': reason}) + else: + raise Exception('reason is not assigned.') + params.update({'appid': appid or self._appid}) + return self._core.request(path, method=RequestType.POST, data=params) + + +def payscore_modify(self, out_order_no, service_id, post_payments, total_amount, reason, post_discounts=None, appid=None): + """修改订单金额 + :param out_order_no: 商户服务订单号,商户系统内部服务订单号(不是交易单号),要求此参数只能由数字、大小写字母_-|*组成,且在同一个商户号下唯一。示例值:'1234323JKHDFE1243252' + :param service_id: 服务ID,该服务ID有本接口对应产品的权限。示例值:'500001' + :param post_payments: 后付费项目,后付费项目列表,最多包含100条付费项目。 + :param total_amount: 总金额,单位为分,不能超过完结订单时候的总金额,只能为整数,详见支付金额。示例值:50000 + :param reason: 取消原因,最多30个字符,每个汉字/数字/英语都按1个字符计算超过长度报错处理。注:重录时需保证参数完全一致,包括取消原因。示例值:'用户投诉' + :param post_discounts: 后付费商户优惠,后付费商户优惠列表,最多包含30条商户优惠。如果传入,用户侧则显示此参数。 + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if out_order_no: + path = '/v3/payscore/serviceorder/%s/modify' % out_order_no + else: + raise Exception('out_order_no is not assigned.') + if service_id: + params.update({'service_id': service_id}) + else: + raise Exception('service_id is not assigned.') + if post_payments: + params.update({'post_payments': post_payments}) + else: + raise Exception('post_payments is not assigned.') + if total_amount: + params.update({'total_amount': total_amount}) + else: + raise Exception('total_amount is not assigned.') + if reason: + params.update({'reason': reason}) + else: + raise Exception('reason is not assigned.') + if post_discounts: + params.update({'post_discounts': post_discounts}) + params.update({'appid': appid or self._appid}) + return self._core.request(path, method=RequestType.POST, data=params) + + +def payscore_complete(self, out_order_no, service_id, post_payments, total_amount, post_discounts=None, + time_range=None, location=None, profit_sharing=False, goods_tag=None, appid=None): + """完结支付分订单 + :param out_order_no: 商户服务订单号,商户系统内部服务订单号(不是交易单号),要求此参数只能由数字、大小写字母_-|*组成,且在同一个商户号下唯一。示例值:'1234323JKHDFE1243252' + :param service_id: 服务ID,该服务ID有本接口对应产品的权限。示例值:'500001' + :param post_payments: 后付费项目,后付费项目列表,最多包含100条付费项目。如果传入,用户侧则显示此参数。 + :param total_amount: 总金额,数字,必须≥0(单位:分),只能为整数。示例值:100 + :param post_discounts: 后付费商户优惠,后付费商户优惠列表,最多包含30条商户优惠。如果传入,用户侧则显示此参数。 + :param time_range: 服务时间范围。 + :param location: 服务位置信息,如果传入,用户侧则显示此参数。 + :param profit_sharing: 微信支付服务分账标记,完结订单分账接口标记。False:不分账,True:分账,默认:False,示例值:False + :param goods_tag: 订单优惠标记,订单优惠标记,代金券或立减金优惠的参数,示例值:'goods_tag' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if out_order_no: + path = '/v3/payscore/serviceorder/%s/complete' % out_order_no + else: + raise Exception('out_order_no is not assigned.') + if service_id: + params.update({'service_id': service_id}) + else: + raise Exception('service_id is not assigned.') + if post_payments: + params.update({'post_payments': post_payments}) + else: + raise Exception('post_payments is not assigned.') + if type(total_amount) is int and total_amount >= 0: + params.update({'total_amount': total_amount}) + else: + raise Exception('total_amount is not assigned.') + if post_discounts: + params.update({'post_discounts': post_discounts}) + if time_range: + params.update({'time_range': time_range}) + if location: + params.update({'location': location}) + if goods_tag: + params.update({'goods_tag': goods_tag}) + params.update({'profit_sharing': profit_sharing}) + params.update({'appid': appid or self._appid}) + return self._core.request(path, method=RequestType.POST, data=params) + + +def payscore_pay(self, out_order_no, service_id, appid=None): + """商户发起催收扣款 + :param out_order_no: 商户服务订单号,商户系统内部服务订单号(不是交易单号),要求此参数只能由数字、大小写字母_-|*组成,且在同一个商户号下唯一。示例值:'1234323JKHDFE1243252' + :param service_id: 服务ID,该服务ID有本接口对应产品的权限。示例值:'500001' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if out_order_no: + path = '/v3/payscore/serviceorder/%s/pay' % out_order_no + else: + raise Exception('out_order_no is not assigned.') + if service_id: + params.update({'service_id': service_id}) + else: + raise Exception('service_id is not assigned.') + params.update({'appid': appid or self._appid}) + return self._core.request(path, method=RequestType.POST, data=params) + + +def payscore_sync(self, out_order_no, service_id, scene_type='Order_Paid', detail={'paid_time': None}, appid=None): + """同步服务订单信息 + :param out_order_no: 商户服务订单号,商户系统内部服务订单号(不是交易单号),要求此参数只能由数字、大小写字母_-|*组成,且在同一个商户号下唯一。示例值:'1234323JKHDFE1243252' + :param service_id: 服务ID,该服务ID有本接口对应产品的权限。示例值:'500001' + :param scene_type: 场景类型,场景类型为“Order_Paid”,表示“订单收款成功” 。示例值:'Order_Paid' + :param detail: 内容信息详情,场景类型为Order_Paid时,为必填项。其中 paid_time表示收款成功时间,示例值:'20091225091210' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + """ + params = {} + if out_order_no: + path = '/v3/payscore/serviceorder/%s/sync' % out_order_no + else: + raise Exception('out_order_no is not assigned.') + if service_id: + params.update({'service_id': service_id}) + else: + raise Exception('service_id is not assigned.') + if scene_type: + params.update({'type': scene_type}) + else: + raise Exception('scene_type is not assigned.') + if detail: + params.update({'detail': detail}) + else: + raise Exception('detail is not assigned.') + params.update({'appid': appid or self._appid}) + return self._core.request(path, method=RequestType.POST, data=params) + + +def payscore_refund(self, transaction_id, out_refund_no, amount, reason=None, + funds_account=None, goods_detail=None, notify_url=None): + """申请退款 + :param transaction_id: 微信支付订单号,示例值:'1217752501201407033233368018' + :param out_refund_no: 商户退款单号,示例值:'1217752501201407033233368018' + :param amount: 金额信息,示例值:{'refund':888, 'total':888, 'currency':'CNY'} + :param reason: 退款原因,示例值:'商品已售完' + :param funds_account: 退款资金来源,示例值:'AVAILABLE' + :param goods_detail: 退款商品,示例值:{'merchant_goods_id':'1217752501201407033233368018', 'wechatpay_goods_id':'1001', 'goods_name':'iPhone6s 16G', 'unit_price':528800, 'refund_amount':528800, 'refund_quantity':1} + :param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php' + """ + return refund(self, out_refund_no=out_refund_no, amount=amount, transaction_id=transaction_id, reason=reason, + funds_account=funds_account, goods_detail=goods_detail, notify_url=notify_url) + + +def payscore_refund_query(self, out_refund_no): + """查询单笔退款 + :param out_refund_no: 商户退款单号,示例值:'1217752501201407033233368018' + """ + return query_refund(self, out_refund_no=out_refund_no) + + +def payscore_merchant_bill(self, bill_date, service_id, tar_type='GZIP', encryption_algorithm='AEAD_AES_256_GCM'): + """商户申请获取对账单 + :param bill_date: 账单日期,格式'YYYY-MM-DD',仅支持下载近三个月的账单。示例值:'2021-01-01' + :param service_id: 支付分服务ID。示例值:'2002000000000558128851361561536' + :param tar_type: 账单的压缩类型,'GZIP':文件压缩方式为gzip,返回.gzip格式的压缩文件。示例值:'GZIP' + :param encryption_algorithm: 加密算法,对返回账单原文加密的算法'AEAD_AES_256_GCM',账单使用AEAD_AES_256_GCM加密算法进行加密。示例值:'AEAD_AES_256_GCM' + """ + if bill_date: + path = '/v3/payscore/merchant-bill?bill_date=%s' % bill_date + else: + raise Exception('bill_date is not assigned.') + if service_id: + path = '%s&service_id=%s' % (path, service_id) + else: + raise Exception('service_id is not assigned.') + path = '%s&tar_type=%s' % (path, tar_type if tar_type else 'GZIP') + path = '%s&encryption_algorithm=%s' % (path, encryption_algorithm if encryption_algorithm else 'AEAD_AES_256_GCM') + return self._core.request(path) diff --git a/wechatpayv3/profitsharing.py b/wechatpayv3/profitsharing.py new file mode 100644 index 0000000..e0f5fed --- /dev/null +++ b/wechatpayv3/profitsharing.py @@ -0,0 +1,518 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType + + +def profitsharing_order(self, transaction_id, out_order_no, receivers, unfreeze_unsplit, + appid=None, sub_appid=None, sub_mchid=None): + """请求分账 + :param transaction_id: 微信支付订单号,示例值:'4208450740201411110007820472' + :param out_order_no: 商户分账单号,只能是数字、大小写字母_-|*@,示例值:'P20150806125346' + :param receivers: 分账接收方列表,最多可有50个分账接收方,示例值:[{'type':'MERCHANT_ID', 'account':'86693852', 'amount':888, 'description':'分给商户A'}] + :param unfreeze_unsplit: 是否解冻剩余未分资金,示例值:True, False + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param sub_appid: (服务商模式)子商户应用ID,示例值:'wxd678efh567hg6999' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if transaction_id: + params.update({'transaction_id': transaction_id}) + else: + raise Exception('transaction_id is not assigned') + if out_order_no: + params.update({'out_order_no': out_order_no}) + else: + raise Exception('out_order_no is not assigned') + if isinstance(unfreeze_unsplit, bool): + params.update({'unfreeze_unsplit': unfreeze_unsplit}) + else: + raise Exception('unfreeze_unsplit is not assigned') + if isinstance(receivers, list): + params.update({'receivers': receivers}) + else: + raise Exception('receivers is not assigned') + cipher_data = False + for receiver in params.get('receivers'): + if receiver.get('name'): + receiver['name'] = self._core.encrypt(receiver.get('name')) + cipher_data = True + params.update({'appid': appid or self._appid}) + if self._partner_mode: + if sub_appid: + params.update({'sub_appid': sub_appid}) + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + path = '/v3/profitsharing/orders' + return self._core.request(path, method=RequestType.POST, data=params, cipher_data=cipher_data) + + +def profitsharing_order_query(self, transaction_id, out_order_no, sub_mchid=None): + """查询分账结果 + :param transaction_id: 微信支付订单号,示例值:'4208450740201411110007820472' + :param out_order_no: 商户分账单号,只能是数字、大小写字母_-|*@,示例值:'P20150806125346' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + if transaction_id and out_order_no: + path = '/v3/profitsharing/orders/%s?transaction_id=%s' % (out_order_no, transaction_id) + else: + raise Exception('transaction_id or out_order_no is not assigned.') + if self._partner_mode: + if sub_mchid: + path = '%s&sub_mchid=%s' % (path, sub_mchid) + else: + raise Exception('sub_mchid is not assigned.') + return self._core.request(path) + + +def profitsharing_return(self, out_return_no, return_mchid, amount, description, + order_id=None, out_order_no=None, sub_mchid=None): + """请求分账回退 + :param out_return_no: 商户回退单号,商户在自己后台生成的一个新的回退单号,在商户后台唯一,示例值:'R20190516001' + :param return_mchid: 回退商户号,分账接口中的分账接收方商户号,示例值:'86693852' + :param amount: 回退金额,单位为分,示例值:888 + :param description: 回退描述,分账回退的原因描述,示例值:'用户退款' + :param order_id: 微信分账单号,与out_order_no参数二选一,示例值:'3008450740201411110007820472' + :param out_order_no: 商户分账单号,只能是数字、大小写字母_-|*@,示例值:'P20150806125346' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if order_id: + params.update({'order_id': order_id}) + elif out_order_no: + params.update({'out_order_no': out_order_no}) + else: + raise Exception('order_id or out_order_no is not assigned.') + if out_return_no: + params.update({'out_return_no': out_return_no}) + else: + raise Exception('out_return_no is not assigned') + if return_mchid: + params.update({'return_mchid': return_mchid}) + else: + raise Exception('return_mchid is not assigned') + if amount: + params.update({'amount': amount}) + else: + raise Exception('amount is not assigned') + if description: + params.update({'description': description}) + else: + raise Exception('description is not assigned') + if self._partner_mode: + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + path = '/v3/profitsharing/return-orders' + return self._core.request(path, method=RequestType.POST, data=params) + + +def profitsharing_return_query(self, out_order_no, out_return_no, sub_mchid=None): + """查询分账回退结果 + :param out_order_no: 商户分账单号,只能是数字、大小写字母_-|*@,示例值:'P20150806125346' + :param out_return_no: 商户回退单号,商户在自己后台生成的一个新的回退单号,在商户后台唯一,示例值:'R20190516001' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + if out_order_no and out_return_no: + path = '/v3/profitsharing/return-orders/%s?&out_order_no=%s' % (out_return_no, out_order_no) + else: + raise Exception('out_order_no or out_return_no is not assigned') + if self._partner_mode: + if sub_mchid: + path = '%s&sub_mchid=%s' % (path, sub_mchid) + else: + raise Exception('sub_mchid is not assigned.') + return self._core.request(path) + + +def profitsharing_unfreeze(self, transaction_id, out_order_no, description, sub_mchid=None): + """解冻剩余资金 + :param transaction_id: 微信支付订单号,示例值:'4208450740201411110007820472' + :param out_order_no: 商户分账单号,只能是数字、大小写字母_-|*@,示例值:'P20150806125346' + :param description: 分账描述,分账的原因描述,分账账单中需要体现,示例值:'解冻全部剩余资金' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if transaction_id: + params.update({'transaction_id': transaction_id}) + else: + raise Exception('transaction_id is not assigned') + if out_order_no: + params.update({'out_order_no': out_order_no}) + else: + raise Exception('out_order_no is not assigned') + if description: + params.update({'description': description}) + else: + raise Exception('description is not assigned') + if self._partner_mode: + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + path = '/v3/profitsharing/orders/unfreeze' + return self._core.request(path, method=RequestType.POST, data=params) + + +def profitsharing_amount_query(self, transaction_id): + """查询剩余待分金额 + :param transaction_id: 微信支付订单号,示例值:'4208450740201411110007820472' + """ + if transaction_id: + path = '/v3/profitsharing/transactions/%s/amounts' % transaction_id + else: + raise Exception('transaction_id is not assigned') + return self._core.request(path) + + +def profitsharing_config_query(self, sub_mchid): + """查询最大分账比例 + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + if sub_mchid: + path = '/v3/profitsharing/merchant-configs/%s' % sub_mchid + else: + raise Exception('sub_mchid is not assigned') + return self._core.request(path) + + +def profitsharing_add_receiver(self, account_type, account, relation_type, name=None, + custom_relation=None, appid=None, sub_appid=None, sub_mchid=None): + """添加分账接收方 + :param account_type: 分账接收方类型,枚举值:'MERCHANT_ID':商户ID,'PERSONAL_OPENID':个人openid + :param account: 分账接收方账号,类型是'MERCHANT_ID'时,是商户号,类型是'PERSONAL_OPENID'时,是个人openid,示例值:'86693852' + :param relation_type:与分账方的关系类型,枚举值:'STORE':门店,'STAFF':员工,'STORE_OWNER':店主, + 'PARTNER':合作伙伴,'HEADQUARTER':总部,'BRAND':品牌方,'DISTRIBUTOR':分销商, + 'USER':用户,'SUPPLIER': 供应商,'CUSTOM':自定义,示例值:'STORE' + :param name: 分账个人接收方姓名,分账接收方类型是'MERCHANT_ID'时,是商户全称(必传),当商户是小微商户或个体户时,是开户人姓名, + 分账接收方类型是'PERSONAL_OPENID'时,是个人姓名 + :param custom_relation: 自定义的分账关系,子商户与接收方具体的关系,本字段最多10个字。当字段'relation_type'的值为'CUSTOM'时,本字段必填; + 当字段'relation_type'的值不为'CUSTOM'时,本字段无需填写。示例值:'代理商' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param sub_appid: (服务商模式)子商户应用ID,示例值:'wxd678efh567hg6999' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if account_type: + params.update({'type': account_type}) + else: + raise Exception('account_type is not assigned') + if account: + params.update({'account': account}) + else: + raise Exception('account is not assigned') + if relation_type: + params.update({'relation_type': relation_type}) + else: + raise Exception('relation_type is not assigned') + cipher_data = False + if name: + params.update({'name': self._core.encrypt(name)}) + cipher_data = True + if relation_type == 'CUSTOM': + if custom_relation: + params.update({'custom_relation': custom_relation}) + else: + raise Exception('custom_relation is not assigned') + params.update({'appid': appid or self._appid}) + if self._partner_mode: + if sub_appid: + params.update({'sub_appid': sub_appid}) + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + path = '/v3/profitsharing/receivers/add' + return self._core.request(path, method=RequestType.POST, data=params, cipher_data=cipher_data) + + +def profitsharing_delete_receiver(self, account_type, account, appid=None, sub_appid=None, sub_mchid=None): + """删除分账接收方 + :param account_type: 分账接收方类型,枚举值:'MERCHANT_ID':商户ID,'PERSONAL_OPENID':个人openid + :param account: 分账接收方账号,类型是'MERCHANT_ID'时,是商户号,类型是'PERSONAL_OPENID'时,是个人openid,示例值:'86693852' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param sub_appid: (服务商模式)子商户应用ID,示例值:'wxd678efh567hg6999' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if account_type: + params.update({'type': account_type}) + else: + raise Exception('account_type is not assigned') + if account: + params.update({'account': account}) + else: + raise Exception('account is not assigned') + params.update({'appid': appid or self._appid}) + if self._partner_mode: + if sub_appid: + params.update({'sub_appid': sub_appid}) + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + path = '/v3/profitsharing/receivers/delete' + return self._core.request(path, method=RequestType.POST, data=params) + + +def profitsharing_bill(self, bill_date, tar_type='GZIP', sub_mchid=None): + """申请分账账单 + :param bill_date: 账单日期,格式'YYYY-MM-DD',仅支持三个月内的账单下载申请。示例值:'2019-06-11' + :param tar_type: 压缩类型,默认值:'GZIP' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + path = '/v3/profitsharing/bills?bill_date=%s&tar_type=%s' % (bill_date, tar_type) + if self._partner_mode and sub_mchid: + path = '%s&sub_mchid=%s' % (path, sub_mchid) + return self._core.request(path) + + +def brand_profitsharing_order(self, brand_mchid, sub_mchid, transaction_id, out_order_no, receivers, + finish, appid=None, sub_appid=None): + """连锁品牌请求分账 + :param brand_mchid: 品牌主商户号,示例值:'1900000108' + :param sub_mchid: 子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + :param transaction_id: 微信支付订单号,示例值:'4208450740201411110007820472' + :param out_order_no: 商户分账单号,只能是数字、大小写字母_-|*@,示例值:'P20150806125346' + :param receivers: 分账接收方列表,最多可有50个分账接收方,示例值:{{'type':'MERCHANT_ID', 'account':'86693852', 'amount':888, 'description':'分给商户A'}} + :param finish: 是否完成分账,示例值:True, False + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param sub_appid: 子商户应用ID,示例值:'wxd678efh567hg6999' + """ + params = {} + if brand_mchid: + params.update({'brand_mchid': brand_mchid}) + else: + raise Exception('brand_mchid is not assigned') + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + if transaction_id: + params.update({'transaction_id': transaction_id}) + else: + raise Exception('transaction_id is not assigned') + if out_order_no: + params.update({'out_order_no': out_order_no}) + else: + raise Exception('out_order_no is not assigned') + if receivers: + params.update({'receivers': receivers}) + else: + raise Exception('receivers is not assigned') + if isinstance(finish, bool): + params.update({'finish': finish}) + else: + raise Exception('finish is not assigned') + params.update({'appid': appid or self._appid}) + if sub_appid: + params.update({'sub_appid': sub_appid}) + path = '/v3/brand/profitsharing/orders' + return self._core.request(path, method=RequestType.POST, data=params) + + +def brand_profitsharing_order_query(self, transaction_id, out_order_no, sub_mchid): + """查询连锁品牌分账结果 + :param transaction_id: 微信支付订单号,示例值:'4208450740201411110007820472' + :param out_order_no: 商户分账单号,只能是数字、大小写字母_-|*@,示例值:'P20150806125346' + :param sub_mchid: 子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + if sub_mchid: + path = '/v3/brand/profitsharing/orders?sub_mchid=%s' % sub_mchid + else: + raise Exception('sub_mchid is not assigned.') + if transaction_id and out_order_no: + path = '%s&transaction_id=%s&out_order_no=%s' % (path, transaction_id, out_order_no) + else: + raise Exception('transaction_id or out_order_no is not assigned.') + return self._core.request(path) + + +def brand_profitsharing_return(self, sub_mchid, out_return_no, return_mchid, amount, + description, order_id=None, out_order_no=None,): + """请求连锁品牌分账回退 + :param sub_mchid: 子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + :param out_return_no: 商户回退单号,商户在自己后台生成的一个新的回退单号,在商户后台唯一,示例值:'R20190516001' + :param return_mchid: 回退商户号,分账接口中的分账接收方商户号,示例值:'86693852' + :param amount: 回退金额,单位为分,示例值:888 + :param description: 回退描述,分账回退的原因描述,示例值:'用户退款' + :param order_id: 微信分账单号,与out_order_no参数二选一,示例值:'3008450740201411110007820472' + :param out_order_no: 商户分账单号,只能是数字、大小写字母_-|*@,示例值:'P20150806125346' + """ + params = {} + if not (order_id and out_order_no): + raise Exception('order_id or out_order_no is not assigned') + if order_id: + params.update({'order_id': order_id}) + elif out_order_no: + params.update({'out_order_no': out_order_no}) + else: + raise Exception('order_id or out_order_no is not assigned.') + if out_return_no: + params.update({'out_return_no': out_return_no}) + else: + raise Exception('out_return_no is not assigned') + if return_mchid: + params.update({'return_mchid': return_mchid}) + else: + raise Exception('return_mchid is not assigned') + if amount: + params.update({'amount': amount}) + else: + raise Exception('amount is not assigned') + if description: + params.update({'description': description}) + else: + raise Exception('description is not assigned') + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + path = '/v3/brand/profitsharing/returnorders' + return self._core.request(path, method=RequestType.POST, data=params) + + +def brand_profitsharing_return_query(self, sub_mchid, out_return_no, order_id=None, out_order_no=None): + """查询连锁品牌分账回退结果 + :param sub_mchid: 子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + :param out_return_no: 商户回退单号,商户在自己后台生成的一个新的回退单号,在商户后台唯一,示例值:'R20190516001' + :param order_id: 微信分账单号,与out_order_no参数二选一,示例值:'3008450740201411110007820472' + :param out_order_no: 商户分账单号,只能是数字、大小写字母_-|*@,示例值:'P20150806125346' + """ + if sub_mchid: + path = '/v3/brand/profitsharing/returnorders?sub_mchid=%s' % sub_mchid + else: + raise Exception('sub_mchid is not assigned.') + if out_return_no: + path = '%s&out_return_no=%s' % (path, out_return_no) + else: + raise Exception('out_return_no is not assigned') + if order_id: + path = '%s&order_id=%s' % (path, order_id) + elif out_order_no: + path = '%s&out_order_no=%s' % (path, out_order_no) + else: + raise Exception('order_id or out_order_no is not assigned.') + return self._core.request(path) + + +def brand_profitsharing_unfreeze(self, sub_mchid, transaction_id, out_order_no, description): + """完结连锁品牌分账 + :param sub_mchid: 子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + :param transaction_id: 微信支付订单号,示例值:'4208450740201411110007820472' + :param out_order_no: 商户分账单号,只能是数字、大小写字母_-|*@,示例值:'P20150806125346' + :param description: 分账描述,分账的原因描述,分账账单中需要体现,示例值:'解冻全部剩余资金' + """ + params = {} + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + if transaction_id: + params.update({'transaction_id': transaction_id}) + else: + raise Exception('transaction_id is not assigned') + if out_order_no: + params.update({'out_order_no': out_order_no}) + else: + raise Exception('out_order_no is not assigned') + if description: + params.update({'description': description}) + else: + raise Exception('description is not assigned') + path = '/v3/brand/profitsharing/finish-order' + return self._core.request(path, method=RequestType.POST, data=params) + + +def brand_profitsharing_amount_query(self, transaction_id): + """查询连锁品牌分账剩余待分金额 + :param transaction_id: 微信支付订单号,示例值:'4208450740201411110007820472' + """ + if transaction_id: + path = '/v3/brand/profitsharing/orders/%s/amounts' % transaction_id + else: + raise Exception('transaction_id is not assigned') + return self._core.request(path) + + +def brand_profitsharing_config_query(self, brand_mchid): + """查询连锁品牌分账最大分账比例 + :param brand_mchid: 品牌商户号,示例值:'1900000108' + """ + if brand_mchid: + path = '/v3/brand/profitsharing/brand-configs/%s' % brand_mchid + else: + raise Exception('brand_mchid is not assigned') + return self._core.request(path) + + +def brand_profitsharing_add_receiver(self, brand_mchid, account_type, account, relation_type, + name=None, appid=None, sub_appid=None): + """添加分账接收方 + :param brand_mchid: 品牌商户号,示例值:'1900000108' + :param account_type: 分账接收方类型,枚举值:'MERCHANT_ID':商户ID,'PERSONAL_OPENID':个人openid + :param account: 分账接收方账号,类型是'MERCHANT_ID'时,是商户号,类型是'PERSONAL_OPENID'时,是个人openid,示例值:'86693852' + :param relation_type:与分账方的关系类型,枚举值:'STORE':门店,'STAFF':员工,'STORE_OWNER':店主, + 'PARTNER':合作伙伴,'HEADQUARTER':总部,'BRAND':品牌方,'DISTRIBUTOR':分销商, + 'USER':用户,'SUPPLIER': 供应商,'CUSTOM':自定义,示例值:'STORE' + :param name: 分账个人接收方姓名,分账接收方类型是'MERCHANT_ID'时,是商户全称(必传),当商户是小微商户或个体户时,是开户人姓名, + 分账接收方类型是'PERSONAL_OPENID'时,是个人姓名 + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param sub_appid: 子商户应用ID,示例值:'wxd678efh567hg6999' + """ + params = {} + if brand_mchid: + params.update({'brand_mchid': brand_mchid}) + else: + raise Exception('brand_mchid is not assigned.') + if account_type: + params.update({'type': account_type}) + else: + raise Exception('account_type is not assigned') + if account: + params.update({'account': account}) + else: + raise Exception('account is not assigned') + if relation_type: + params.update({'relation_type': relation_type}) + else: + raise Exception('relation_type is not assigned') + cipher_data = False + if name: + params.update({'name': self._core.encrypt(name)}) + cipher_data = True + params.update({'appid': appid or self._appid}) + if sub_appid: + params.update({'sub_appid': sub_appid}) + path = '/v3/brand/profitsharing/receivers/add' + return self._core.request(path, method=RequestType.POST, data=params, cipher_data=cipher_data) + + +def brand_profitsharing_delete_receiver(self, brand_mchid, account_type, account, appid=None, sub_appid=None): + """删除连锁品牌分账接收方 + :param brand_mchid: 品牌商户号,示例值:'1900000108' + :param account_type: 分账接收方类型,枚举值:'MERCHANT_ID':商户ID,'PERSONAL_OPENID':个人openid + :param account: 分账接收方账号,类型是'MERCHANT_ID'时,是商户号,类型是'PERSONAL_OPENID'时,是个人openid,示例值:'86693852' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param sub_appid: (服务商模式)子商户应用ID,示例值:'wxd678efh567hg6999' + """ + params = {} + if brand_mchid: + params.update({'brand_mchid': brand_mchid}) + else: + raise Exception('brand_mchid is not assigned.') + if account_type: + params.update({'type': account_type}) + else: + raise Exception('account_type is not assigned') + if account: + params.update({'account': account}) + else: + raise Exception('account is not assigned') + params.update({'appid': appid or self._appid}) + if sub_appid: + params.update({'sub_appid': sub_appid}) + path = '/v3/profitsharing/receivers/delete' + return self._core.request(path, method=RequestType.POST, data=params) diff --git a/wechatpayv3/smartguide.py b/wechatpayv3/smartguide.py new file mode 100644 index 0000000..780e039 --- /dev/null +++ b/wechatpayv3/smartguide.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType + + +def guides_register(self, corpid, store_id, userid, name, mobile, qr_code, avatar, group_qrcode=None, sub_mchid=None): + """服务人员注册 + :param corpid: 企业ID, 示例值:'1234567890' + :param store_id: 门店ID, 示例值:12345678 + :param userid: 企业微信的员工ID, 示例值:'robert' + :param name: 企业微信的员工姓名, 示例值:'robert' + :param mobile: 手机号码, 示例值:'13900000000' + :param qr_code: 员工个人二维码, 示例值:'https://open.work.weixin.qq.com/wwopen/userQRCode?vcode=xxx' + :param avatar: 头像URL, 示例值:'http://wx.qlogo.cn/mmopen/ajNVdqHZLLA3WJ6DSZUfiakYe37PKnQhBIeOQBO4czqrnZDS79FH5Wm5m4X69TBicnHFlhiafvDwklOpZeXYQQ2icg/0' + :param group_qrcode: 群二维码URL, 示例值:'http://p.qpic.cn/wwhead/nMl9ssowtibVGyrmvBiaibzDtp/0' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if corpid: + params.update({'corpid': corpid}) + else: + raise Exception('corpid is not assigned.') + if store_id: + params.update({'store_id': store_id}) + else: + raise Exception('store_id is not assigned.') + if userid: + params.update({'userid': userid}) + else: + raise Exception('userid is not assigned.') + if name: + params.update({'name': self._core.encrypt(name)}) + else: + raise Exception('name is not assigned') + if mobile: + params.update({'mobile': self._core.encrypt(mobile)}) + else: + raise Exception('mobile is not assigned.') + if qr_code: + params.update({'qr_code': qr_code}) + else: + raise Exception('qr_code is not assigned.') + if avatar: + params.update({'avatar': avatar}) + else: + raise Exception('avatar is not assigned.') + if group_qrcode: + params.update({'group_qrcode': group_qrcode}) + if self._partner_mode and sub_mchid: + params.update({'sub_mchid': sub_mchid}) + path = '/v3/smartguide/guides' + return self._core.request(path, method=RequestType.POST, data=params, cipher_data=True) + + +def guides_assign(self, guide_id, out_trade_no, sub_mchid=None): + """服务人员分配 + :param guide_id: 服务人员ID,示例值:'LLA3WJ6DSZUfiaZDS79FH5Wm5m4X69TBic' + :param out_trade_no: 商户订单号, 示例值:'20150806125346' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if out_trade_no: + params.update({'out_trade_no': out_trade_no}) + else: + raise Exception('out_trade_no is not assigned.') + if self._partner_mode and sub_mchid: + params.update({'sub_mchid': sub_mchid}) + if guide_id: + path = '/v3/smartguide/guides/%s/assign' % guide_id + else: + raise Exception('guide_id is not assigned.') + return self._core.request(path, method=RequestType.POST, data=params) + + +def guides_query(self, store_id, userid=None, mobile=None, work_id=None, limit=None, offset=0, sub_mchid=None): + """服务人员查询 + :params store_id: 门店ID, 示例值:1234 + :params userid: 企业微信的员工ID, 示例值:'robert' + :params mobile: 手机号码, 示例值:'13900000000' + :params work_id: 工号, 示例值:'robert' + :params limit: 最大资源条数, 示例值:5 + :params offset: 请求资源起始位置, 示例值:0 + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + if not store_id: + raise Exception('store_id is not assigned.') + path = '/v3/smartguide/guides?store_id=%s' % store_id + if userid: + path = '%s&userid=%s' % (path, userid) + cipher_data = False + if mobile: + path = '%s&mobile=%s' % (path, self._core.encrypt(mobile)) + cipher_data = True + if work_id: + path = '%s&work_id=%s' % (path, work_id) + if limit: + path = '%s&limit=%s' % (path, limit) + if offset: + path = '%s&offset=%s' % (path, offset) + if self._partner_mode and sub_mchid: + path = '%s&sub_mchid=%s' % (path, sub_mchid) + return self._core.request(path, cipher_data=cipher_data) + + +def guides_update(self, guide_id, name=None, mobile=None, qr_code=None, avatar=None, group_qrcode=None, sub_mchid=None): + """服务人员信息更新 + :params guide_id: 服务人员ID, 示例值:'LLA3WJ6DSZUfiaZDS79FH5Wm5m4X69TBic' + :params name: 服务人员姓名, 示例值:'robert' + :params mobile: 服务人员手机号码, 示例值:'13900000000' + :params qr_code: 服务人员二维码URL, 示例值:'https://open.work.weixin.qq.com/wwopen/userQRCode?vcode=xxx' + :params avatar: 服务人员头像URL, 示例值:'http://wx.qlogo.cn/mmopen/ajNVdqHZLLA3WJ6DSZUfiakYe37PKnQhBIeOQBO4czqrnZDS79FH5Wm5m4X69TBicnHFlhiafvDwklOpZeXYQQ2icg/0' + :params group_qrcode: 群二维码URL, 示例值:'http://p.qpic.cn/wwhead/nMl9ssowtibVGyrmvBiaibzDtp/0' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if not guide_id: + raise Exception('guide_id is not assigned.') + path = '/v3/smartguide/guides/%s' % guide_id + cipher_data = False + if name: + params.update({'name': self._core.encrypt(name)}) + cipher_data = True + if mobile: + params.update({'mobile': self._core.encrypt(mobile)}) + cipher_data = True + if qr_code: + params.update({'qr_code': qr_code}) + if avatar: + params.update({'avatar': avatar}) + if group_qrcode: + params.update({'group_qrcode': group_qrcode}) + if self._partner_mode and sub_mchid: + params.update({'sub_mchid': sub_mchid}) + return self._core.request(path, method=RequestType.PATCH, data=params, cipher_data=cipher_data) diff --git a/wechatpayv3/transaction.py b/wechatpayv3/transaction.py new file mode 100644 index 0000000..8a968f2 --- /dev/null +++ b/wechatpayv3/transaction.py @@ -0,0 +1,458 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType, WeChatPayType + + +def pay(self, + description, + out_trade_no, + amount, + payer=None, + time_expire=None, + attach=None, + goods_tag=None, + detail=None, + scene_info=None, + settle_info=None, + notify_url=None, + appid=None, + mchid=None, + sub_appid=None, + sub_mchid=None, + support_fapiao=False, + pay_type=None): + """统一下单 + :return code, message: + :param description: 商品描述,示例值:'Image形象店-深圳腾大-QQ公仔' + :param out_trade_no: 商户订单号,示例值:'1217752501201407033233368018' + :param amount: 订单金额,示例值:{'total':100, 'currency':'CNY'} + :param payer: 支付者,示例值:{'openid':'oHkLxtx0vUqe-18p_AXTZ1innxkCY'} + :param time_expire: 交易结束时间,示例值:'2018-06-08T10:34:56+08:00' + :param attach: 附加数据,示例值:'自定义数据' + :param goods_tag: 订单优惠标记,示例值:'WXG' + :param detail: 优惠功能,示例值:{'cost_price':608800, 'invoice_id':'微信123', 'goods_detail':[{'merchant_goods_id':'商品编码', 'wechatpay_goods_id':'1001', 'goods_name':'iPhoneX 256G', 'quantity':1, 'unit_price':828800}]} + :param scene_info: 场景信息,示例值:{'payer_client_ip':'14.23.150.211', 'device_id':'013467007045764', 'store_info':{'id':'0001', 'name':'腾讯大厦分店', 'area_code':'440305', 'address':'广东省深圳市南山区科技中一道10000号'}} + :param settle_info: 结算信息,示例值:{'profit_sharing':False} + :param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param mchid: 微信支付商户号,可不填,默认传入初始化的mchid,示例值:'987654321' + :param sub_appid: (服务商模式)子商户应用ID,示例值:'wxd678efh567hg6999' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + :param support_fapiao: 电子发票入口开放标识,传入true时,支付成功消息和支付详情页将出现开票入口。 + :param pay_type: 微信支付类型,示例值:WeChatPayType.JSAPI + """ + params = {} + if pay_type != WeChatPayType.CODEPAY: + if not (notify_url or self._notify_url): + raise Exception('notify_url is not assigned.') + params.update({'notify_url': notify_url or self._notify_url}) + if description: + params.update({'description': description}) + else: + raise Exception('description is not assigned.') + if out_trade_no: + params.update({'out_trade_no': out_trade_no}) + else: + raise Exception('out_trade_no is not assigned.') + if amount: + params.update({'amount': amount}) + else: + raise Exception('amount is not assigned.') + if payer: + params.update({'payer': payer}) + if scene_info: + params.update({'scene_info': scene_info}) + if time_expire: + params.update({'time_expire': time_expire}) + if attach: + params.update({'attach': attach}) + if goods_tag: + params.update({'goods_tag': goods_tag}) + if detail: + params.update({'detail': detail}) + if settle_info: + params.update({'settle_info': settle_info}) + pay_type = pay_type or self._type + if self._partner_mode: + params.update({'sp_appid': appid or self._appid}) + params.update({'sp_mchid': mchid or self._mchid}) + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + if sub_appid: + params.update({'sub_appid': sub_appid}) + if pay_type in [WeChatPayType.JSAPI, WeChatPayType.MINIPROG]: + if not payer: + raise Exception('payer is not assigned') + path = '/v3/pay/partner/transactions/jsapi' + elif pay_type == WeChatPayType.APP: + path = '/v3/pay/partner/transactions/app' + elif pay_type == WeChatPayType.H5: + if not scene_info: + raise Exception('scene_info is not assigned.') + path = '/v3/pay/partner/transactions/h5' + elif pay_type == WeChatPayType.NATIVE: + path = '/v3/pay/partner/transactions/native' + elif pay_type == WeChatPayType.CODEPAY: + path = '/v3/pay/partner/transactions/codepay' + else: + raise Exception('pay_type is not assigned.') + else: + params.update({'appid': appid or self._appid}) + params.update({'mchid': mchid or self._mchid}) + if pay_type in [WeChatPayType.JSAPI, WeChatPayType.MINIPROG]: + if not payer: + raise Exception('payer is not assigned') + path = '/v3/pay/transactions/jsapi' + elif pay_type == WeChatPayType.APP: + path = '/v3/pay/transactions/app' + elif pay_type == WeChatPayType.H5: + if not scene_info: + raise Exception('scene_info is not assigned.') + path = '/v3/pay/transactions/h5' + elif pay_type == WeChatPayType.NATIVE: + path = '/v3/pay/transactions/native' + elif pay_type == WeChatPayType.CODEPAY: + path = '/v3/pay/transactions/codepay' + else: + raise Exception('pay_type is not assigned.') + if support_fapiao: + params.update({'support_fapiao': support_fapiao}) + return self._core.request(path, method=RequestType.POST, data=params) + + +def close(self, out_trade_no, mchid=None, sub_mchid=None): + """关闭订单 + :param out_trade_no: 商户订单号,示例值:'1217752501201407033233368018' + :param mchid: 微信支付商户号,可不传,默认传入初始化的mchid。示例值:'987654321' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + if self._partner_mode: + if out_trade_no: + path = '/v3/pay/partner/transactions/out-trade-no/%s/close' % out_trade_no + else: + raise Exception('out_trade_no is not assigned.') + if sub_mchid: + params = {'sp_mchid': mchid or self._mchid, 'sub_mchid': sub_mchid} + else: + raise Exception('sub_mchid is not assigned.') + else: + if out_trade_no: + path = '/v3/pay/transactions/out-trade-no/%s/close' % out_trade_no + else: + raise Exception('out_trade_no is not assigned.') + params = {'mchid': mchid or self._mchid} + return self._core.request(path, method=RequestType.POST, data=params) + + +def query(self, transaction_id=None, out_trade_no=None, mchid=None, sub_mchid=None): + """查询订单 + :param transaction_id: 微信支付订单号,示例值:1217752501201407033233368018 + :param out_trade_no: 商户订单号,示例值:1217752501201407033233368018 + :param mchid: 微信支付商户号,可不传,默认传入初始化的mchid。示例值:'987654321' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + if self._partner_mode: + if transaction_id: + path = '/v3/pay/partner/transactions/id/%s' % transaction_id + elif out_trade_no: + path = '/v3/pay/partner/transactions/out-trade-no/%s' % out_trade_no + else: + raise Exception('transaction_id or out_trade_no is not assigned.') + path = '%s?sp_mchid=%s&sub_mchid=%s' % (path, mchid or self._mchid, sub_mchid) + else: + if transaction_id: + path = '/v3/pay/transactions/id/%s' % transaction_id + elif out_trade_no: + path = '/v3/pay/transactions/out-trade-no/%s' % out_trade_no + else: + raise Exception('transaction_id out_trade_no is not assigned.') + path = '%s?mchid=%s' % (path, mchid or self._mchid) + return self._core.request(path) + + +def refund(self, + out_refund_no, + amount, + transaction_id=None, + out_trade_no=None, + reason=None, + funds_account=None, + goods_detail=None, + notify_url=None, + sub_mchid=None): + """申请退款 + :param out_refund_no: 商户退款单号,示例值:'1217752501201407033233368018' + :param amount: 金额信息,示例值:{'refund':888, 'total':888, 'currency':'CNY'} + :param transaction_id: 微信支付订单号,示例值:'1217752501201407033233368018' + :param out_trade_no: 商户订单号,示例值:'1217752501201407033233368018' + :param reason: 退款原因,示例值:'商品已售完' + :param funds_account: 退款资金来源,示例值:'AVAILABLE' + :param goods_detail: 退款商品,示例值:{'merchant_goods_id':'1217752501201407033233368018', 'wechatpay_goods_id':'1001', 'goods_name':'iPhone6s 16G', 'unit_price':528800, 'refund_amount':528800, 'refund_quantity':1} + :param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if notify_url or self._notify_url: + params.update({'notify_url': notify_url or self._notify_url}) + if out_refund_no: + params.update({'out_refund_no': out_refund_no}) + else: + raise Exception('out_refund_no is not assigned.') + if amount: + params.update({'amount': amount}) + else: + raise Exception('amount is not assigned.') + if transaction_id: + params.update({'transaction_id': transaction_id}) + elif out_trade_no: + params.update({'out_trade_no': out_trade_no}) + else: + raise Exception('transaction_id is not assigned.') + if reason: + params.update({'reason': reason}) + if funds_account: + params.update({'funds_account': funds_account}) + if goods_detail: + params.update({'goods_detail': goods_detail}) + if self._partner_mode: + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + path = '/v3/refund/domestic/refunds' + return self._core.request(path, method=RequestType.POST, data=params) + + +def query_refund(self, out_refund_no, sub_mchid=None): + """查询单笔退款 + :param out_refund_no: 商户退款单号,示例值:'1217752501201407033233368018' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + path = '/v3/refund/domestic/refunds/%s' % out_refund_no + if self._partner_mode: + if sub_mchid: + path = '%s?sub_mchid=%s' % (path, sub_mchid) + else: + raise Exception('sub_mchid is not assigned.') + return self._core.request(path) + + +def trade_bill(self, bill_date, bill_type='ALL', tar_type='GZIP', sub_mchid=None): + """申请交易账单 + :param bill_date: 账单日期,示例值:'2019-06-11' + :param bill_type: 账单类型, 默认值:'ALL' + :param tar_type: 压缩类型,默认值:'GZIP' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + path = '/v3/bill/tradebill?bill_date=%s&bill_type=%s&tar_type=%s' % (bill_date, bill_type, tar_type) + if self._partner_mode and sub_mchid: + path = '%s&sub_mchid=%s' % (path, sub_mchid) + return self._core.request(path) + + +def fundflow_bill(self, bill_date, account_type='BASIC', tar_type='GZIP'): + """申请资金账单 + :param bill_date: 账单日期,示例值:'2019-06-11' + :param account_type: 资金账户类型, 默认值:'BASIC',基本账户, 可选:'OPERATION',运营账户;'FEES',手续费账户 + :param tar_type: 压缩类型,默认值:'GZIP' + """ + if not bill_date: + raise Exception('bill_date is not assigned.') + path = '/v3/bill/fundflowbill?bill_date=%s&account_type=%s&tar_type=%s' % (bill_date, account_type, tar_type) + return self._core.request(path) + + +def submch_fundflow_bill(self, sub_mchid, bill_date, account_type, algorithm='AEAD_AES_256_GCM', tar_type=None): + """申请单个子商户资金账单 + :param sub_mchid: 子商户号,示例值:'19000000001' + :param bill_date: 账单日期,格式YYYY-MM-DD,示例值:'2019-06-11' + :param account_type: 资金账户类型,枚举值:'BASIC':基本账户,'OPERATION':运营账户,'FEES':手续费账户,示例值:'BASIC' + :param algorithm: 加密算法,枚举值:'AEAD_AES_256_GCM':AEAD_AES_256_GCM加密算法 + :param tar_type: 压缩格式,枚举值:'GZIP':返回格式为.gzip的压缩包账单 + """ + path = '/v3/bill/sub-merchant-fundflowbill' + if sub_mchid: + path += '?sub_mchid=%s' % sub_mchid + else: + raise Exception('sub_mchid is not assigned.') + if bill_date: + path += '&bill_date=%s' % bill_date + else: + raise Exception('bill_date is not assigned.') + if account_type: + path += '&account_type=%s' % account_type + else: + raise Exception('account_type is not assigned.') + if algorithm: + path += '&algorithm=%s' % algorithm + else: + raise Exception('algorithm is not assigned.') + if tar_type: + path += '&tar_type=%s' % tar_type + return self._core.request(path) + + +def download_bill(self, url): + """下载账单 + :param url: 账单下载地址,示例值:'https://api.mch.weixin.qq.com/v3/billdownload/file?token=xxx' + """ + path = url[len(self._core._gate_way):] if url.startswith(self._core._gate_way) else url + return self._core.request(path, skip_verify=True) + + +def combine_pay(self, + combine_out_trade_no, + sub_orders, + scene_info=None, + combine_payer_info=None, + time_start=None, + time_expire=None, + combine_appid=None, + combine_mchid=None, + notify_url=None, + pay_type=None): + """合单支付下单 + :param combine_out_trade_no: 合单商户订单号, 示例值:'P20150806125346' + :param sub_orders: 子单信息,示例值:[{'mchid':'1900000109', 'attach':'深圳分店', 'amount':{'total_amount':100,'currency':'CNY'}, 'out_trade_no':'20150806125346', 'description':'腾讯充值中心-QQ会员充值', 'settle_info':{'profit_sharing':False, 'subsidy_amount':10}}] + :param scene_info: 场景信息, 示例值:{'device_id':'POS1:123', 'payer_client_ip':'14.17.22.32'} + :param combine_payer_info: 支付者, 示例值:{'openid':'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o'} + :param time_start: 交易起始时间,示例值:'2019-12-31T15:59:59+08:00' + :param time_expire: 交易结束时间, 示例值:'2019-12-31T15:59:59+08:00' + :param combine_appid: 合单商户appid, 示例值:'wxd678efh567hg6787' + :param combine_mchid: 合单发起方商户号,示例值:'1900000109' + :param notify_url: 通知地址, 示例值:'https://yourapp.com/notify' + :param pay_type: 微信支付类型,示例值:WeChatPayType.JSAPI + """ + params = {} + params.update({'combine_appid': combine_appid or self._appid}) + params.update({'combine_mchid': combine_mchid or self._mchid}) + if not (notify_url or self._notify_url): + raise Exception('notify_url is not assigned.') + params.update({'notify_url': notify_url or self._notify_url}) + if combine_out_trade_no: + params.update({'combine_out_trade_no': combine_out_trade_no}) + else: + raise Exception('combine_out_trade_no is not assigned.') + if sub_orders: + params.update({'sub_orders': sub_orders}) + else: + raise Exception('sub_orders is not assigned.') + if scene_info: + params.update({'scene_info': scene_info}) + if combine_payer_info: + params.update({'combine_payer_info': combine_payer_info}) + if time_start: + params.update({'time_start': time_start}) + if time_expire: + params.update({'time_expire': time_expire}) + pay_type = pay_type or self._type + if pay_type in [WeChatPayType.JSAPI, WeChatPayType.MINIPROG]: + if not combine_payer_info: + raise Exception('combine_payer_info is not assigned') + path = '/v3/combine-transactions/jsapi' + elif pay_type == WeChatPayType.APP: + path = '/v3/combine-transactions/app' + elif pay_type == WeChatPayType.H5: + if not scene_info: + raise Exception('scene_info is not assigned.') + path = '/v3/combine-transactions/h5' + elif pay_type == WeChatPayType.NATIVE: + path = '/v3/combine-transactions/native' + else: + raise Exception('pay_type is not assigned.') + return self._core.request(path, method=RequestType.POST, data=params) + + +def combine_query(self, combine_out_trade_no): + """合单查询订单 + :param combine_out_trade_no: 合单商户订单号,示例值:P20150806125346 + """ + params = {} + if not combine_out_trade_no: + raise Exception('combine_out_trade_no is not assigned') + else: + params.update({'combine_out_trade_no': combine_out_trade_no}) + path = '/v3/combine-transactions/out-trade-no/%s' % combine_out_trade_no + return self._core.request(path) + + +def combine_close(self, combine_out_trade_no, sub_orders, combine_appid=None): + """合单关闭订单 + :param combine_out_trade_no: 合单商户订单号,示例值:'P20150806125346' + :param sub_orders: 子单信息, 示例值:[{'mchid': '1900000109', 'out_trade_no': '20150806125346'}] + :param combine_appid: 合单商户appid, 示例值:'wxd678efh567hg6787' + """ + params = {} + params.update({'combine_appid': combine_appid or self._appid}) + if not combine_out_trade_no: + raise Exception('combine_out_trade_no is not assigned.') + if not sub_orders: + raise Exception('sub_orders is not assigned.') + else: + params.update({'sub_orders': sub_orders}) + path = '/v3/combine-transactions/out-trade-no/%s/close' % combine_out_trade_no + return self._core.request(path, method=RequestType.POST, data=params) + + +def abnormal_refund(self, refund_id, out_refund_no, type, bank_type=None, bank_account=None, real_name=None, sub_mchid=None): + """发起异常退款 + :param refund_id: 微信退款单号,退款单的主键,唯一定义此资源的标识。 + :param out_refund_no: 商户退款单号,商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。 + :param type: 异常退款处理方式,可选值:'USER_BANK_CARD',退款到用户银行卡; 'MERCHANT_BANK_CARD',退款至交易商户银行账户。 + :param bank_type: 开户银行类型,采用字符串类型的银行标识,值列表详见官网银行类型。 + :param bank_account: 收款银行卡号,用户的银行卡账号。 + :param real_name: 收款用户姓名。 + """ + if refund_id: + path = '/v3/refund/domestic/refunds/%s/apply-abnormal-refund' % refund_id + else: + raise Exception('refund_id is not assigned.') + params = {} + if self._partner_mode: + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + if not (out_refund_no and type): + raise Exception('out_refund_no or type is not assigned.') + params.update({'out_refund_no': out_refund_no}) + params.update({'type': type}) + if bank_type: + params.update({'bank_type': bank_type}) + cipher_data = False + if bank_account: + params.update({'bank_account': self._core.encrypt(bank_account)}) + cipher_data = True + if real_name: + params.update({'real_name': self._core.encrypt(real_name)}) + cipher_data = True + return self._core.request(path, method=RequestType.POST, data=params, cipher_data=cipher_data) + +def codepay_reverse(self, out_trade_no, appid=None, mchid=None, sub_appid=None, sub_mchid=None): + """撤销付款码支付订单 + :警告:付款码支付订单如果用户已经付款,调用撤销接口会将资金退回给用户。: + :return code, message: + :param out_trade_no: 商户订单号,示例值:'1217752501201407033233368018' + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param mchid: 微信支付商户号,可不填,默认传入初始化的mchid,示例值:'987654321' + :param sub_appid: (服务商模式)子商户应用ID,示例值:'wxd678efh567hg6999' + :param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109' + """ + params = {} + if self._partner_mode: + params.update({'sp_appid': appid or self._appid}) + params.update({'sp_mchid': mchid or self._mchid}) + if sub_mchid: + params.update({'sub_mchid': sub_mchid}) + else: + raise Exception('sub_mchid is not assigned.') + if sub_appid: + params.update({'sub_appid': sub_appid}) + path = f'/v3/pay/partner/transactions/out-trade-no/{out_trade_no}/reverse' + else: + params.update({'appid': appid or self._appid}) + params.update({'mchid': mchid or self._mchid}) + path = f'/v3/pay/transactions/out-trade-no/{out_trade_no}/reverse' + return self._core.request(path, method=RequestType.POST, data=params) diff --git a/wechatpayv3/transfer.py b/wechatpayv3/transfer.py new file mode 100644 index 0000000..73dc5f2 --- /dev/null +++ b/wechatpayv3/transfer.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- + +from .type import RequestType + + +def transfer_batch(self, out_batch_no, batch_name, batch_remark, total_amount, total_num, transfer_detail_list=[], appid=None, transfer_scene_id=None, notify_url=None): + """发起商家转账 + :param out_batch_no: 商户系统内部的商家批次单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一,示例值:'plfk2020042013' + :param batch_name: 该笔批量转账的名称,示例值:'2019年1月深圳分部报销单' + :param batch_remark: 转账说明,UTF8编码,最多允许32个字符,示例值:'2019年1月深圳分部报销单' + :param total_amount: 转账总金额,单位为分,必须与批次内所有明细转账金额之和保持一致,否则无法发起转账操作,示例值:'4000000' + :param total_num: 转账总笔数,必须与批次内所有明细之和保持一致,否则无法发起转账操作,示例值:200 + :param transfer_detail_list: 发起批量转账的明细列表,最多三千笔,示例值:[{"out_detail_no": "x23zy545Bd5436", "transfer_amount": 200000, "transfer_remark": "2020年4月报销", "openid": "o-MYE42l80oelYMDE34nYD456Xoy", "user_name": "张三"}] + :param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef' + :param transfer_scene_id: 转账场景ID,示例值:'1001' + :param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php' + """ + params = {} + if out_batch_no: + params.update({'out_batch_no': out_batch_no}) + else: + raise Exception('out_batch_no is not assigned') + if batch_name: + params.update({'batch_name': batch_name}) + else: + raise Exception('batch_name is not assigned') + if batch_remark: + params.update({'batch_remark': batch_remark}) + else: + raise Exception('batch_remark is not assigned') + if total_amount: + params.update({'total_amount': total_amount}) + else: + raise Exception('total_amount is not assigned') + if total_num: + params.update({'total_num': total_num}) + else: + raise Exception('total_num is not assigned') + if transfer_detail_list: + params.update({'transfer_detail_list': transfer_detail_list}) + else: + raise Exception('transfer_detail_list is not assigned') + cipher_data = False + for transfer_detail in params.get('transfer_detail_list'): + if transfer_detail.get('user_name'): + transfer_detail['user_name'] = self._core.encrypt(transfer_detail.get('user_name')) + cipher_data = True + params.update({'appid': appid or self._appid}) + if notify_url or self._notify_url: + params.update({'notify_url': notify_url or self._notify_url}) + if transfer_scene_id: + params.update({'transfer_scene_id': transfer_scene_id}) + path = '/v3/transfer/batches' + return self._core.request(path, method=RequestType.POST, data=params, cipher_data=cipher_data) + + +def transfer_query_batchid(self, batch_id, need_query_detail=False, offset=0, limit=20, detail_status='ALL'): + """微信批次单号查询批次单 + :param batch_id: 微信批次单号,微信商家转账系统返回的唯一标识,示例值:1030000071100999991182020050700019480001 + :param need_query_detail: 是否查询转账明细单,枚举值:true:是;false:否,默认否。 + :param offset: 请求资源起始位置,默认值为0 + :param limit: 最大资源条数,默认值为20 + :param detail_status: 明细状态, ALL:全部。需要同时查询转账成功和转账失败的明细单;SUCCESS:转账成功。只查询转账成功的明细单;FAIL:转账失败。 + """ + if batch_id: + path = '/v3/transfer/batches/batch-id/%s' % batch_id + else: + raise Exception('batch_id is not assigned') + if need_query_detail: + path += '?need_query_detail=true' + path += '&detail_status=%s' % detail_status + else: + path += '?need_query_detail=false' + path += '&offset=%s' % offset + path += '&limit=%s' % limit + return self._core.request(path) + + +def transfer_query_detail_id(self, batch_id, detail_id): + """微信明细单号查询明细单 + :param batch_id: 微信批次单号,微信商家转账系统返回的唯一标识,示例值:1030000071100999991182020050700019480001 + :param detail_id: 微信明细单号,微信支付系统内部区分转账批次单下不同转账明细单的唯一标识,示例值:1040000071100999991182020050700019500100 + """ + if batch_id and detail_id: + path = '/v3/transfer/batches/batch-id/%s/details/detail-id/%s' % (batch_id, detail_id) + else: + raise Exception('batch_id or detail_id is not assigned') + return self._core.request(path) + + +def transfer_query_out_batch_no(self, out_batch_no, need_query_detail=False, offset=0, limit=20, detail_status='ALL'): + """商家批次单号查询批次单 + :param out_batch_no: 商家批次单号,示例值:plfk2020042013 + :param need_query_detail: 是否查询转账明细单,枚举值:true:是;false:否,默认否。 + :param offset: 请求资源起始位置,默认值为0 + :param limit: 最大资源条数,默认值为20 + :param detail_status: 明细状态, ALL:全部。需要同时查询转账成功和转账失败的明细单;SUCCESS:转账成功。只查询转账成功的明细单;FAIL:转账失败。 + """ + if out_batch_no: + path = '/v3/transfer/batches/out-batch-no/%s' % out_batch_no + else: + raise Exception('batch_id is not assigned') + if need_query_detail: + path += '?need_query_detail=true' + path += '&detail_status=%s' % detail_status + else: + path += '?need_query_detail=false' + path += '&offset=%s' % offset + path += '&limit=%s' % limit + return self._core.request(path) + + +def transfer_query_out_detail_no(self, out_detail_no, out_batch_no): + """商家明细单号查询明细单 + :param out_detail_no: 商家明细单号,示例值:x23zy545Bd5436 + :param out_batch_no: 商家批次单号,示例值:plfk2020042013 + """ + if out_detail_no and out_batch_no: + path = '/v3/transfer/batches/out-batch-no/%s/details/out-detail-no/%s' % (out_batch_no, out_detail_no) + else: + raise Exception('out_detail_no or out_batch_no is not assigned') + return self._core.request(path) + + +def transfer_bill_receipt(self, out_batch_no): + """转账电子回单申请受理 + :param out_batch_no: 商家批次单号,示例值:plfk2020042013 + """ + params = {} + if out_batch_no: + params.update({'out_batch_no': out_batch_no}) + else: + raise Exception('out_batch_no is assigned') + path = '/v3/transfer/bill-receipt' + return self._core.request(path, method=RequestType.POST, params=params) + + +def transfer_query_bill_receipt(self, out_batch_no): + """查询转账电子回单 + :param out_batch_no: 商家批次单号,示例值:plfk2020042013 + """ + if out_batch_no: + path = '/v3/transfer/bill-receipt/%s' % out_batch_no + else: + raise Exception('out_batch_no is not assigned') + return self._core.request(path) + + +def transfer_detail_receipt(self, accept_type, out_detail_no, out_batch_no=None,): + """转账明细电子回单受理 + :param accept_type: 受理类型 + :param out_detail_no: 商家明细单号,示例值:x23zy545Bd5436 + :param out_batch_no: 商家批次单号,示例值:plfk2020042013 + """ + params = {} + if accept_type: + params.update({'accept_type': accept_type}) + else: + raise Exception('accept_type is not assigned') + if out_detail_no: + params.update({'out_detail_no': out_detail_no}) + else: + raise Exception('out_detail_no is not assigned') + if out_batch_no: + params.update({'out_batch_no': out_batch_no}) + path = '/v3/transfer-detail/electronic-receipts' + return self._core.request(path, method=RequestType.POST, params=params) + + +def transfer_query_receipt(self, accept_type, out_detail_no, out_batch_no=None): + """查询转账明细电子回单受理结果 + :param accept_type: 受理类型 + :param out_detail_no: 商家明细单号,示例值:x23zy545Bd5436 + :param out_batch_no: 商家批次单号,示例值:plfk2020042013 + """ + if accept_type: + path = '/v3/transfer-detail/electronic-receipts?accept_type=%s' % accept_type + else: + raise Exception('accept_type is not assigned') + if out_detail_no: + path += '&out_batch_no=%s' % out_detail_no + else: + raise Exception('out_detail_no is not assigned') + if out_batch_no: + path += '&out_batch_no=%s' % out_batch_no + return self._core.request(path) diff --git a/wechatpayv3/type.py b/wechatpayv3/type.py new file mode 100644 index 0000000..fc2e986 --- /dev/null +++ b/wechatpayv3/type.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +from enum import Enum, unique + + +@unique +class RequestType(Enum): + GET = 'GET' + POST = 'POST' + PATCH = 'PATCH' + PUT = 'PUT' + DELETE = 'DELETE' + + +class WeChatPayType(Enum): + JSAPI = 0 + APP = 1 + H5 = 2 + NATIVE = 3 + MINIPROG = 4 + CODEPAY = 5 + + +class SignType(Enum): + RSA_SHA256 = 0 + HMAC_SHA256 = 1 + MD5 = 2 diff --git a/wechatpayv3/utils.py b/wechatpayv3/utils.py new file mode 100644 index 0000000..e783379 --- /dev/null +++ b/wechatpayv3/utils.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- + +import json +import time +import uuid +from base64 import b64decode, b64encode + +from cryptography.exceptions import InvalidSignature, InvalidTag +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP, PKCS1v15 +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.hazmat.primitives.hashes import SHA1, SHA256, SM3, Hash +from cryptography.hazmat.primitives.hmac import HMAC +from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key +from cryptography.x509 import load_pem_x509_certificate +from cryptography import __version__ as cryptography_version + + +def build_authorization(path, + method, + mchid, + serial_no, + private_key, + data=None, + nonce_str=None): + timeStamp = str(int(time.time())) + nonce_str = nonce_str or ''.join(str(uuid.uuid4()).split('-')).upper() + body = data if isinstance(data, str) else json.dumps(data) if data else '' + sign_str = '%s\n%s\n%s\n%s\n%s\n' % (method, path, timeStamp, nonce_str, body) + signature = rsa_sign(private_key=private_key, sign_str=sign_str) + authorization = 'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",signature="%s",timestamp="%s",serial_no="%s"' % (mchid, nonce_str, signature, timeStamp, serial_no) + return authorization + + +def rsa_sign(private_key, sign_str): + message = sign_str.encode('UTF-8') + signature = private_key.sign(data=message, padding=PKCS1v15(), algorithm=SHA256()) + sign = b64encode(signature).decode('UTF-8').replace('\n', '') + return sign + + +def aes_decrypt(nonce, ciphertext, associated_data, apiv3_key): + key_bytes = apiv3_key.encode('UTF-8') + nonce_bytes = nonce.encode('UTF-8') + associated_data_bytes = associated_data.encode('UTF-8') + data = b64decode(ciphertext) + aesgcm = AESGCM(key=key_bytes) + try: + result = aesgcm.decrypt(nonce=nonce_bytes, data=data, associated_data=associated_data_bytes).decode('UTF-8') + except InvalidTag: + result = None + return result + + +def format_private_key(private_key_str): + pem_start = '-----BEGIN PRIVATE KEY-----\n' + pem_end = '\n-----END PRIVATE KEY-----' + private_key_str = private_key_str.strip() + if not private_key_str.startswith(pem_start): + private_key_str = pem_start + private_key_str + if not private_key_str.endswith(pem_end): + private_key_str = private_key_str + pem_end + return private_key_str + + +def format_public_key(public_key_str): + pem_start = '-----BEGIN PUBLIC KEY-----\n' + pem_end = '\n-----END PUBLIC KEY-----' + public_key_str = public_key_str.strip() + if not public_key_str.startswith(pem_start): + public_key_str = pem_start + public_key_str + if not public_key_str.endswith(pem_end): + public_key_str = public_key_str + pem_end + return public_key_str + + +def load_certificate(certificate_str): + try: + return load_pem_x509_certificate(data=certificate_str.encode('UTF-8'), backend=default_backend()) + except: + return None + + +def load_private_key(private_key_str): + if not private_key_str: + return None + try: + return load_pem_private_key(data=format_private_key(private_key_str).encode('UTF-8'), password=None, backend=default_backend()) + except: + raise Exception('failed to load private key.') + + +def load_public_key(public_key_str): + if not public_key_str: + return None + try: + return load_pem_public_key(data=format_public_key(public_key_str).encode('UTF-8'), backend=default_backend()) + except: + raise Exception('failed to load public key.') + + +def rsa_verify(timestamp, nonce, body, signature, public_key): + sign_str = '%s\n%s\n%s\n' % (timestamp, nonce, body) + message = sign_str.encode('UTF-8') + try: + signature = b64decode(signature) + except: + return False + try: + public_key.verify(signature, message, PKCS1v15(), SHA256()) + except InvalidSignature: + return False + return True + + +def rsa_encrypt(text, public_key): + data = text.encode('UTF-8') + cipherbyte = public_key.encrypt( + plaintext=data, + padding=OAEP(mgf=MGF1(algorithm=SHA1()), algorithm=SHA1(), label=None) + ) + return b64encode(cipherbyte).decode('UTF-8') + + +def rsa_decrypt(ciphertext, private_key): + data = private_key.decrypt( + ciphertext=b64decode(ciphertext), + padding=OAEP(mgf=MGF1(algorithm=SHA1()), algorithm=SHA1(), label=None) + ) + result = data.decode('UTF-8') + return result + + +def hmac_sign(key, sign_str): + hmac = HMAC(key.encode('UTF-8'), SHA256()) + hmac.update(sign_str.encode('UTF-8')) + sign = hmac.finalize().hex().upper() + return sign + + +def sha256(data): + hash = Hash(SHA256()) + hash.update(data) + return hash.finalize().hex() + + +def sm3(data): + hash = Hash(SM3()) + hash.update(data) + return hash.finalize().hex()