Metadata-Version: 2.4
Name: keble-referral
Version: 1.1.6
Author-email: zhenhao-ma <bob0103779@gmail.com>
Requires-Python: <3.14,>=3.13
Requires-Dist: keble-db<2.0.0,>=1.10.0
Requires-Dist: keble-exceptions<1.0.0,>=0,>=0.0.9
Requires-Dist: keble-helpers<2.0.0,>=1.54.0
Requires-Dist: keble-payment<4.0.0,>=3.0.0
Description-Content-Type: text/markdown

# keble-referral

`keble-referral` owns referral-code, referrer, wallet, policy, commission, and
withdrawal-history schemas plus the Mongo/Redis-backed `ReferralClient`.

## Exact Money Contract

`keble-referral 1.1.6` interprets payment captures, commissions, wallet
balances, locked balances, and withdrawal request integers as the wallet
currency's ISO minor units. Percentage commissions use exact Decimal basis-point
math with one explicit round-down to prevent over-credit. All stored and
response Money values use the shared exponent (JPY 0, USD 2, BHD 3); no wallet
path divides by 100 or rebuilds a float.

The package consumes `keble-payment>=3.0.0,<4.0.0`. Payment 3 keeps the
commission callback contract while making typed order kind and fulfillment
state canonical, so the referral callback remains idempotent against the same
persisted order identity.

Withdrawal amounts must be strictly positive canonical minor units; invalid
zero or negative requests fail schema validation before any wallet mutation or
provider call.

Referral policy threshold and fixed-commission currencies must match the
policy marketplace currency. Invalid policies fail at schema admission rather
than later during a paid callback or wallet tier comparison.

## Mongo Startup Index Contract

`ReferralClient.ainit(amongo=...)` creates idempotent Mongo indexes for the
backend-used referral reads: owner/referrer lookups, referral code lookups,
wallet by marketplace, withdrawal history by wallet/status/transaction, paid
history by referrer/referee/wallet/order, and marketplace policy reads.

Side effect if changes: `keble.backend.app.main.init_db()` calls this hook
before APIs/workers serve traffic; do not create these indexes inside request
handlers or CRUD hot paths.

Paid-order commission application is ACID and retry-safe: history insertion and
wallet credit occur in one Mongo transaction, and new rows carry partial-unique
application/identity keys. The partial indexes deliberately exclude historical
rows that predate those keys, so legacy duplicate wallets or histories cannot
block startup; every new order can still credit at most once under concurrent
callback delivery.

## Testing

This repo follows the root `TESTING_GUIDELINE.md`.
Detailed test structure notes live in `docs/testing.md`.

Local-full development command:

```bash
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"
```

Run schema-only unit tests:

```bash
uv run pytest -m unit
```

Run 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
```

Integration tests require:

```text
MONGO_DB_URI or MONGO_TEST_URI
REDIS_URI or REDIS_TEST_URI
```

The test settings resolve those aliases through `keble_db.testing.resolve_test_env`,
so clean worktrees can reuse credentials from the umbrella `keble.backend/.env`
without committing package-local secrets.

Run the Python syntax/type gate:

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

Latest 2026-07-22 release proof: payment 3 dependency resolution, local-full
pytest, Pyright, Ruff, and wheel/sdist gates passed.

Previous 2026-07-18 proof: exact Money edge slice `11 passed`; portable
`70 passed, 284 deselected`; isolated local-full `355 passed`; isolated
integration `284 passed, 71 deselected`; Pyright `0 errors`; Ruff, lock, and
wheel/sdist build gates passed.

Layer mapping:

- `tests/unit/schemas/` is the fast `unit` layer when tests do not request DB fixtures.
- `tests/integration/crud/`, `tests/integration/main/`, and `tests/integration/workflows/` are guarded `integration`, `db`, `mongo`, `redis`, and `local_stack` tests.
- Future referral quality or policy-regression datasets belong under `tests/evals` with `eval`; selecting `-m eval` is enough, so do not add a generic `RUN_EVALS` gate.
- Mongo databases, collection names, and Redis namespaces are generated per test with `keble_db.testing.create_test_namespace`; do not reintroduce broad shared DB cleanup.
- External withdrawal execution remains mocked by default; do not add real payment-provider calls to default tests.
