Metadata-Version: 2.4
Name: keble-payment
Version: 3.0.1
Author-email: zhenhao-ma <bob0103779@gmail.com>
Requires-Python: <3.14,>=3.13
Requires-Dist: httpx[socks]<1.0.0,>=0
Requires-Dist: keble-db<2.0.0,>=1.8.1
Requires-Dist: keble-exceptions<1.0.0,>=0,>=0.0.9
Requires-Dist: keble-helpers<2.0.0,>=1.54.0
Requires-Dist: python-alipay-sdk==1.10.1
Requires-Dist: stripe<15.0.0,>=14
Description-Content-Type: text/markdown

# keble-payment

`keble-payment` owns payment provider integration schemas and helpers for
Alipay, Stripe, and payment persistence used by Keble services.

## Verified State

Checked on 2026-07-22 against `pyproject.toml`, `keble_payment/`, and tests
on `fix/admin-payment-integrity`.

- Package: `keble-payment 3.0.1`
- Python: `>=3.13,<3.14`
- Critical deps: `keble-helpers`, `keble-db`, `keble-exceptions`,
  `python-alipay-sdk`, `stripe`, `httpx`
- Source roots: `alipay/`, `stripe/`, `paypal/`, `schemas/`, `crud/`, `main/`
- Public exports: provider modules, payment schemas, and main client/helpers.

## Runtime Contract

- `keble-payment 3.0.1` reuses the structurally equivalent legacy fulfillment
  index during startup, regardless of its old name. This keeps upgraded Mongo
  databases from failing with `IndexOptionsConflict` before APIs or workers
  start. Fresh databases create the canonical descriptive index name.
- `keble-payment 3.0.0` makes one typed
  `AdminBankTransferPurchaseCreate` command the only prepaid-order creation
  contract. Callers can no longer override the invariant pair
  `ADMIN_BANK_TRANSFER` + `BANK_TRANSACTION`; simulation similarly derives
  `ADMIN_SIMULATED` + `FAKE` internally. This intentional public API break is
  why the release uses a new major version.
- Real bank references are normalized (Unicode, whitespace, and case) and
  protected by a partial unique compound index scoped to the receivable
  account. The request UUID remains independently unique and fingerprinted, so
  both bank-owned and client-owned idempotency identities are enforced.
- Fulfillment now leases `PENDING` work as `PROCESSING`, records attempts and a
  bounded last error, returns active leases without concurrent replay, and
  permits expired leases to be resumed from durable server state. Only the
  current claim can mark `COMPLETED`; interrupted effects return to `PENDING`.
- `keble-payment 2.5.0` introduced EXPLICIT typed fake/real tagging.
  `TransactionMethod.FAKE` marks admin-simulated non-revenue payments;
  `OrderKind` (STANDARD / ADMIN_SIMULATED / ADMIN_BANK_TRANSFER) on
  `OrderBase.kind` is the CANONICAL fake-order detection — the note prefix
  from `build_admin_simulated_purchase_note` remains human audit text only.
  Legacy rows lack `kind` and default to STANDARD on read; the keble v3
  backend startup migration (`payment_fake_method`) backfills fake-noted
  orders/payments each boot. Legacy maintenance lines `0.0.15.post1` /
  `1.2.4.post1` carry the same FAKE enum member because payment rows live in
  a shared Mongo (v1/v2 backends enum-validate reads).
  `adeactivate_admin_simulated_paid_orders_for_owner` filters by kind and can
  never touch ADMIN_BANK_TRANSFER (real) or STANDARD orders.
- Receivable (money-in) bank accounts are centralized:
  `ReceivableBankAccount*` schemas + `__keble_payment__receivable_bank_account__`
  collection + `OrderClient.acreate/aget/alist/aupdate_receivable_bank_account`
  (ACTIVE/INACTIVE disable-never-delete; historical `bank_transaction` rows
  link via the new `BankTransactionBase.receivable_account` id and keep the
  denormalized `recipient_bank_account` snapshot).
- `acreate_prepaid_order(create=...)` records REAL admin bank transfers end to
  end: required bank reference and receivable account, optional service
  identity, account status/currency/marketplace validation, exact `paid_at`,
  paid-order Redis cache, and payment-success callbacks. Recovery uses
  `aresume_prepaid_order_fulfillment(request_id=...)` and
  `alist_recoverable_prepaid_orders(...)`; callers never reconstruct financial
  payloads from browser state.
- `keble-payment 2.4.3` writes payment, cancellation, deactivation, and coupon
  comparison instants from the shared aware UTC clock. Never persist a naive
  local `datetime.now()` value: Mongo interprets naive values as UTC and can
  shift Asia/Shanghai payment times eight hours into the future, hiding a
  newly paid membership from enabled-period queries.
- `keble-payment 2.4.1` converts PayPal decimal strings through exact
  `Money.from_major_units` / `Money.to_major_units` APIs. Provider output uses
  each currency's audited scale (JPY 0, USD 2, BHD 3) instead of a hard-coded
  two-decimal formatter; unrepresentable precision fails before payout.
- Alipay CNY transfer, WAP checkout, real notification, and admin-replay amounts
  use one immutable `AlipayAmount` schema. Provider values remain decimal text;
  no amount crosses binary float or truncating `int(value * 100)` conversion.
- Provider API details stay in provider modules. Backend checkout flows should
  consume package schemas and helpers rather than embedding provider payloads.
- Stripe stored models support optional `stripe_account` for multi-account
  handling; preserve that field across schema changes.
- Tests must not perform live payment requests in the local-full or
  portable-offline suites. Provider
  fixtures/assets belong under tests and must stay deterministic.
- Enabled provider live tests fail on provider request/schema errors; skips are
  only for missing live-test preconditions such as credentials or test price IDs.
- Reusable test settings and asset helpers live in `keble_payment.testing`.
- Payment tests resolve shared test env values through `keble_db.testing`, so
  backend-level `.env` values can be reused without duplicating secrets in this
  package.
- `OrderPricingPolicySnapshot` is the immutable paid-order audit shape for
  service pricing policies. The snapshot records the selected policy/tag,
  original/base/deal prices, currency, validity window, and optional linked
  claim clue so backend callbacks can consume limited deals only after success.
- Coupon histories snapshot currency, marketplace, and normalized tag/contributor
  attribution at payment success. Multiple tags split 10,000 basis points with
  deterministic largest-remainder allocation, so coupon/tag/contributor totals
  reconcile exactly.
- `CouponListQuery`/`CouponPage` own server search, sort, and pagination. Coupon
  search uses `coupon_search_text_idx`; attribution review uses
  `attribution_status_created_desc_idx`; supported sort fields have explicit indexes.
- `CouponPerformanceQuery` is the only reporting contract. It uses closed-open
  time ranges, IANA timezones, DAY/MONTH/TOTAL buckets, and exact Decimal output
  grouped separately by currency and marketplace.
- Historical attribution changes are explicit human-approved backfills through
  `abackfill_coupon_history_attribution`; never infer current tags during reports.
- Mongo and Redis integration tests must use per-test namespaces/databases and
  cleanup, not broad session-level deletes against shared databases.
- Executable tests stay under canonical layer roots: `tests/unit`,
  `tests/integration`, `tests/live`, `tests/contract`, `tests/evals`, and
  `tests/db_stack`.

## Testing

Detailed test-layer notes live in `docs/testing.md`.

Local-full development command:

```bash
uv sync --group test
RUN_INTEGRATION=1 RUN_REAL_DB=1 RUN_LOCAL_STACK=1 RUN_DB_STACK=1 \
  uv run pytest -q -m "not live and not container"
```

Portable-offline command:

```bash
uv run pytest -q -m "not live and not slow and not eval and not local_stack and not db_stack and not container"
```

Real Mongo/Redis integration tests:

```bash
RUN_INTEGRATION=1 RUN_REAL_DB=1 RUN_LOCAL_STACK=1 uv run pytest -q -m integration
```

Eval selector:

```bash
uv run pytest -q -m eval
```

Syntax/type gate:

```bash
npx --yes pyright .
```

Latest release proof on 2026-07-22:

- Local-full selector: `383 passed, 6 deselected` in `14.42s`.
- `npx --yes pyright .`: `0 errors`.
- Ruff gate and wheel/sdist build passed.

Previous provider-selector proof on 2026-07-18:

- Local-full selector: `360 passed, 6 deselected` in `8.51s`.
- Portable-offline selector: `291 passed, 75 deselected` in `1.64s`.
- Unit selector: `291 passed, 75 deselected` in `1.73s`.
- Eval selector: `366 deselected` in `0.07s` (no eval tests are registered).
- Contract selector: `366 deselected` in `0.07s` (no contract tests are registered).
- DB-stack selector: `366 deselected` (no DB-stack-only tests are registered).
- Integration selector: `69 passed, 297 deselected` in `5.94s`.
- Stripe live canary: `1 passed, 3 skipped` in `3.65s`; skipped cases lacked
  `STRIPE_TEST_PRICE_ID`.
- Alipay live canary: `2 skipped` in `1.46s`; local Alipay key material was unavailable.
- `npx --yes pyright .`: `0 errors`.

## Change Logs

Historical release notes and long AI change history live under `logs/` when
archived.

## Side Effect If Changes

- Backend checkout, membership upgrade, payment callback, and admin order
  routes depend on these provider schemas.
- Frontend purchase success tracking depends on backend payment state derived
  from this package.
