cli-anything-brissoto-pay 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cli_anything_brissoto_pay-1.0.0/.gitignore +24 -0
- cli_anything_brissoto_pay-1.0.0/.python-version +1 -0
- cli_anything_brissoto_pay-1.0.0/PKG-INFO +84 -0
- cli_anything_brissoto_pay-1.0.0/README.md +73 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/__init__.py +3 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/__main__.py +5 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/brissoto_pay_cli.py +195 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/commands/__init__.py +1 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/commands/apps.py +144 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/commands/go_live.py +75 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/commands/methods.py +58 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/commands/payments.py +120 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/commands/servers.py +164 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/commands/webhooks.py +80 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/core/__init__.py +0 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/core/client.py +91 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/core/config.py +35 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/core/errors.py +19 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/core/gateway.py +109 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/core/login.py +118 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/core/runtime.py +52 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/core/session.py +62 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/skills/SKILL.md +156 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/tests/__init__.py +0 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/tests/conftest.py +63 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/tests/fakes.py +89 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/tests/test_apps.py +308 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/tests/test_client.py +68 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/tests/test_errors.py +56 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/tests/test_gateway.py +176 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/tests/test_go_live.py +177 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/tests/test_login.py +326 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/tests/test_logout.py +60 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/tests/test_main.py +39 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/tests/test_methods.py +110 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/tests/test_payments.py +235 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/tests/test_refresh.py +85 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/tests/test_repl.py +66 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/tests/test_servers.py +259 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/tests/test_skill.py +67 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/tests/test_webhooks.py +118 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/tests/test_whoami.py +105 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/utils/__init__.py +0 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/utils/options.py +36 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/utils/output.py +40 -0
- cli_anything_brissoto_pay-1.0.0/cli_anything/brissoto_pay/utils/repl.py +53 -0
- cli_anything_brissoto_pay-1.0.0/pyproject.toml +33 -0
- cli_anything_brissoto_pay-1.0.0/uv.lock +722 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
node_modules
|
|
2
|
+
.DS_Store
|
|
3
|
+
dist
|
|
4
|
+
dist-ssr
|
|
5
|
+
*.local
|
|
6
|
+
.env
|
|
7
|
+
.nitro
|
|
8
|
+
.tanstack
|
|
9
|
+
.wrangler
|
|
10
|
+
.output
|
|
11
|
+
.vinxi
|
|
12
|
+
__unconfig*
|
|
13
|
+
todos.json
|
|
14
|
+
/coverage
|
|
15
|
+
/test-results
|
|
16
|
+
/playwright-report
|
|
17
|
+
.env.prod.convex
|
|
18
|
+
.expo
|
|
19
|
+
|
|
20
|
+
# Python CLI (cli/)
|
|
21
|
+
.venv
|
|
22
|
+
__pycache__
|
|
23
|
+
.pytest_cache
|
|
24
|
+
.coverage
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cli-anything-brissoto-pay
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Set apps up in the Brissoto Pay gateway from a coding agent.
|
|
5
|
+
Project-URL: Documentation, https://pay.brissoto.com/docs
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: click>=8.1
|
|
9
|
+
Requires-Dist: convex>=0.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# cli-anything-brissoto-pay
|
|
13
|
+
|
|
14
|
+
Sets an app up in the Brissoto Pay gateway, from a coding agent or by hand. Admins only.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
Needs [uv](https://docs.astral.sh/uv/) (`brew install uv`). Nothing to install in the project: `uvx` runs the latest release.
|
|
19
|
+
|
|
20
|
+
## With a coding agent
|
|
21
|
+
|
|
22
|
+
1. Open [pay.brissoto.com/docs](https://pay.brissoto.com/docs), click **Copy** on "Set up with a coding agent".
|
|
23
|
+
2. Paste it into your coding agent (Claude Code, Codex, Cursor) in the app's project.
|
|
24
|
+
3. Approve the sign-in in your browser, then answer its questions.
|
|
25
|
+
|
|
26
|
+
It starts in test and asks you before replacing a key or going live.
|
|
27
|
+
|
|
28
|
+
## By hand
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
uvx cli-anything-brissoto-pay login # approve in the browser, 30 days
|
|
32
|
+
uvx cli-anything-brissoto-pay apps create --name Heyama --color "#0F766E" --language fr
|
|
33
|
+
uvx cli-anything-brissoto-pay apps get heyama # the Dev server's id
|
|
34
|
+
uvx cli-anything-brissoto-pay servers update <server> --webhook-url https://api.heyama.com/webhooks/pay --return-url http://localhost:3000/paid
|
|
35
|
+
uvx cli-anything-brissoto-pay servers key <server>
|
|
36
|
+
uvx cli-anything-brissoto-pay servers webhook-secret <server>
|
|
37
|
+
uvx cli-anything-brissoto-pay webhooks test <server>
|
|
38
|
+
uvx cli-anything-brissoto-pay go-live heyama --webhook-url https://api.heyama.com/webhooks/pay --return-url https://heyama.com/paid --yes
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Put the key and secret in `BRISSOTO_PAY_API_KEY` and `BRISSOTO_PAY_WEBHOOK_SECRET`, in an env file that is not committed.
|
|
42
|
+
|
|
43
|
+
## Commands
|
|
44
|
+
|
|
45
|
+
| Command | Does |
|
|
46
|
+
| --------------------------------------------------------------- | ---------------------------------------- |
|
|
47
|
+
| `apps list` | Every app |
|
|
48
|
+
| `apps get <slug>` | One app, its servers, URLs and key hints |
|
|
49
|
+
| `apps create --name --color --language [--logo]` | A Test app with a Dev server |
|
|
50
|
+
| `apps branding <slug> [--name] [--color] [--language] [--logo]` | Change its brand |
|
|
51
|
+
| `servers add <slug> <name>` | A test server |
|
|
52
|
+
| `servers update <server> [--webhook-url] [--return-url ...]` | Its URLs |
|
|
53
|
+
| `servers key <server> [--yes]` | Its API key, shown once |
|
|
54
|
+
| `servers webhook-secret <server> [--yes]` | Its webhook secret, shown once |
|
|
55
|
+
| `methods list [--env] [--app]` | Payment methods and their providers |
|
|
56
|
+
| `methods set <slug> <method> --on\|--off` | Offer a method in test, or not |
|
|
57
|
+
| `webhooks test <server> [--wait]` | Send a `webhook.test`, say how it went |
|
|
58
|
+
| `payments list <slug> [--env] [--days] [--status] [--page]` | The app's payments |
|
|
59
|
+
| `payments get <id>` | One payment, its attempts and webhooks |
|
|
60
|
+
| `go-live <slug> [--webhook-url] [--return-url ...] --yes` | Production server, live key and secret |
|
|
61
|
+
|
|
62
|
+
Replacing a key or secret, and `go-live`, fail with `confirmation_required` without `--yes`.
|
|
63
|
+
|
|
64
|
+
No command opens the REPL. Every command takes `--json` and `--help`.
|
|
65
|
+
|
|
66
|
+
| Option | Env var | Default |
|
|
67
|
+
| -------------- | ------------------------- | ------------------------ |
|
|
68
|
+
| `--gateway` | `BRISSOTO_PAY_GATEWAY` | `prod` |
|
|
69
|
+
| `--convex-url` | `BRISSOTO_PAY_CONVEX_URL` | the gateway's Convex URL |
|
|
70
|
+
| `--web-url` | `BRISSOTO_PAY_WEB_URL` | the gateway's dashboard |
|
|
71
|
+
|
|
72
|
+
| Gateway | Convex URL | Dashboard |
|
|
73
|
+
| ------- | -------------------------------------- | -------------------------- |
|
|
74
|
+
| `prod` | `https://api-pay.brissoto.com` | `https://pay.brissoto.com` |
|
|
75
|
+
| `dev` | `https://giant-bison-875.convex.cloud` | `http://localhost:4310` |
|
|
76
|
+
|
|
77
|
+
Sessions: `~/.config/brissoto-pay/sessions.json`, one per Convex URL.
|
|
78
|
+
|
|
79
|
+
## Develop
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
npm run test:cli
|
|
83
|
+
uv run --directory cli cli-anything-brissoto-pay --help
|
|
84
|
+
```
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# cli-anything-brissoto-pay
|
|
2
|
+
|
|
3
|
+
Sets an app up in the Brissoto Pay gateway, from a coding agent or by hand. Admins only.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Needs [uv](https://docs.astral.sh/uv/) (`brew install uv`). Nothing to install in the project: `uvx` runs the latest release.
|
|
8
|
+
|
|
9
|
+
## With a coding agent
|
|
10
|
+
|
|
11
|
+
1. Open [pay.brissoto.com/docs](https://pay.brissoto.com/docs), click **Copy** on "Set up with a coding agent".
|
|
12
|
+
2. Paste it into your coding agent (Claude Code, Codex, Cursor) in the app's project.
|
|
13
|
+
3. Approve the sign-in in your browser, then answer its questions.
|
|
14
|
+
|
|
15
|
+
It starts in test and asks you before replacing a key or going live.
|
|
16
|
+
|
|
17
|
+
## By hand
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
uvx cli-anything-brissoto-pay login # approve in the browser, 30 days
|
|
21
|
+
uvx cli-anything-brissoto-pay apps create --name Heyama --color "#0F766E" --language fr
|
|
22
|
+
uvx cli-anything-brissoto-pay apps get heyama # the Dev server's id
|
|
23
|
+
uvx cli-anything-brissoto-pay servers update <server> --webhook-url https://api.heyama.com/webhooks/pay --return-url http://localhost:3000/paid
|
|
24
|
+
uvx cli-anything-brissoto-pay servers key <server>
|
|
25
|
+
uvx cli-anything-brissoto-pay servers webhook-secret <server>
|
|
26
|
+
uvx cli-anything-brissoto-pay webhooks test <server>
|
|
27
|
+
uvx cli-anything-brissoto-pay go-live heyama --webhook-url https://api.heyama.com/webhooks/pay --return-url https://heyama.com/paid --yes
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Put the key and secret in `BRISSOTO_PAY_API_KEY` and `BRISSOTO_PAY_WEBHOOK_SECRET`, in an env file that is not committed.
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
| Command | Does |
|
|
35
|
+
| --------------------------------------------------------------- | ---------------------------------------- |
|
|
36
|
+
| `apps list` | Every app |
|
|
37
|
+
| `apps get <slug>` | One app, its servers, URLs and key hints |
|
|
38
|
+
| `apps create --name --color --language [--logo]` | A Test app with a Dev server |
|
|
39
|
+
| `apps branding <slug> [--name] [--color] [--language] [--logo]` | Change its brand |
|
|
40
|
+
| `servers add <slug> <name>` | A test server |
|
|
41
|
+
| `servers update <server> [--webhook-url] [--return-url ...]` | Its URLs |
|
|
42
|
+
| `servers key <server> [--yes]` | Its API key, shown once |
|
|
43
|
+
| `servers webhook-secret <server> [--yes]` | Its webhook secret, shown once |
|
|
44
|
+
| `methods list [--env] [--app]` | Payment methods and their providers |
|
|
45
|
+
| `methods set <slug> <method> --on\|--off` | Offer a method in test, or not |
|
|
46
|
+
| `webhooks test <server> [--wait]` | Send a `webhook.test`, say how it went |
|
|
47
|
+
| `payments list <slug> [--env] [--days] [--status] [--page]` | The app's payments |
|
|
48
|
+
| `payments get <id>` | One payment, its attempts and webhooks |
|
|
49
|
+
| `go-live <slug> [--webhook-url] [--return-url ...] --yes` | Production server, live key and secret |
|
|
50
|
+
|
|
51
|
+
Replacing a key or secret, and `go-live`, fail with `confirmation_required` without `--yes`.
|
|
52
|
+
|
|
53
|
+
No command opens the REPL. Every command takes `--json` and `--help`.
|
|
54
|
+
|
|
55
|
+
| Option | Env var | Default |
|
|
56
|
+
| -------------- | ------------------------- | ------------------------ |
|
|
57
|
+
| `--gateway` | `BRISSOTO_PAY_GATEWAY` | `prod` |
|
|
58
|
+
| `--convex-url` | `BRISSOTO_PAY_CONVEX_URL` | the gateway's Convex URL |
|
|
59
|
+
| `--web-url` | `BRISSOTO_PAY_WEB_URL` | the gateway's dashboard |
|
|
60
|
+
|
|
61
|
+
| Gateway | Convex URL | Dashboard |
|
|
62
|
+
| ------- | -------------------------------------- | -------------------------- |
|
|
63
|
+
| `prod` | `https://api-pay.brissoto.com` | `https://pay.brissoto.com` |
|
|
64
|
+
| `dev` | `https://giant-bison-875.convex.cloud` | `http://localhost:4310` |
|
|
65
|
+
|
|
66
|
+
Sessions: `~/.config/brissoto-pay/sessions.json`, one per Convex URL.
|
|
67
|
+
|
|
68
|
+
## Develop
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
npm run test:cli
|
|
72
|
+
uv run --directory cli cli-anything-brissoto-pay --help
|
|
73
|
+
```
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""`cli-anything-brissoto-pay`: Click entry point and REPL."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.resources import files
|
|
6
|
+
from typing import NoReturn
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from .commands.apps import apps
|
|
11
|
+
from .commands.go_live import go_live
|
|
12
|
+
from .commands.methods import methods
|
|
13
|
+
from .commands.payments import payments
|
|
14
|
+
from .commands.servers import servers
|
|
15
|
+
from .commands.webhooks import webhooks
|
|
16
|
+
from .core.client import signed_out
|
|
17
|
+
from .core.config import (
|
|
18
|
+
CONVEX_URL_ENV,
|
|
19
|
+
GATEWAY_ENV,
|
|
20
|
+
GATEWAYS,
|
|
21
|
+
PROD,
|
|
22
|
+
WEB_URL_ENV,
|
|
23
|
+
)
|
|
24
|
+
from .core.errors import CliError
|
|
25
|
+
from .core.login import new_secret, pkce_challenge, sign_in_url, wait_for_code
|
|
26
|
+
from .core.runtime import Runtime
|
|
27
|
+
from .utils.options import json_option, pass_runtime
|
|
28
|
+
from .utils.output import emit, emit_error
|
|
29
|
+
from .utils.repl import run_repl
|
|
30
|
+
|
|
31
|
+
class BrissotoPayGroup(click.Group):
|
|
32
|
+
"""Turns a `CliError` into the error shape and a non-zero exit."""
|
|
33
|
+
|
|
34
|
+
def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
|
|
35
|
+
ctx.meta["argv"] = list(args)
|
|
36
|
+
try:
|
|
37
|
+
return super().parse_args(ctx, args)
|
|
38
|
+
except click.UsageError as error:
|
|
39
|
+
self._usage_error(ctx, error)
|
|
40
|
+
|
|
41
|
+
def invoke(self, ctx: click.Context):
|
|
42
|
+
runtime = ctx.find_object(Runtime)
|
|
43
|
+
try:
|
|
44
|
+
return super().invoke(ctx)
|
|
45
|
+
except CliError as error:
|
|
46
|
+
emit_error(runtime.json, error)
|
|
47
|
+
ctx.exit(1)
|
|
48
|
+
except click.UsageError as error:
|
|
49
|
+
self._usage_error(ctx, error)
|
|
50
|
+
|
|
51
|
+
def _usage_error(self, ctx: click.Context, error: click.UsageError) -> NoReturn:
|
|
52
|
+
"""With `--json`, a usage error in the error shape, exit 2; else Click's own."""
|
|
53
|
+
runtime = ctx.find_object(Runtime)
|
|
54
|
+
if not (runtime.json or "--json" in ctx.meta["argv"]):
|
|
55
|
+
raise error
|
|
56
|
+
emit_error(True, CliError("usage", error.format_message()))
|
|
57
|
+
ctx.exit(2)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@click.group(cls=BrissotoPayGroup, invoke_without_command=True)
|
|
61
|
+
@click.option("--json", "as_json", is_flag=True, help="Answer in JSON.")
|
|
62
|
+
@click.option(
|
|
63
|
+
"--gateway",
|
|
64
|
+
type=click.Choice(list(GATEWAYS)),
|
|
65
|
+
envvar=GATEWAY_ENV,
|
|
66
|
+
default=PROD,
|
|
67
|
+
show_default=True,
|
|
68
|
+
help="Deployment: its Convex URL and dashboard.",
|
|
69
|
+
)
|
|
70
|
+
@click.option(
|
|
71
|
+
"--convex-url",
|
|
72
|
+
envvar=CONVEX_URL_ENV,
|
|
73
|
+
help="Convex URL, instead of the gateway's.",
|
|
74
|
+
)
|
|
75
|
+
@click.option(
|
|
76
|
+
"--web-url",
|
|
77
|
+
envvar=WEB_URL_ENV,
|
|
78
|
+
help="Dashboard that approves the sign-in, instead of the gateway's.",
|
|
79
|
+
)
|
|
80
|
+
@click.pass_context
|
|
81
|
+
def cli(
|
|
82
|
+
ctx: click.Context,
|
|
83
|
+
as_json: bool,
|
|
84
|
+
gateway: str,
|
|
85
|
+
convex_url: str | None,
|
|
86
|
+
web_url: str | None,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Set apps up in the Brissoto Pay gateway. No command opens the REPL."""
|
|
89
|
+
runtime = ctx.ensure_object(Runtime)
|
|
90
|
+
runtime.json = as_json
|
|
91
|
+
default_convex_url, default_web_url = GATEWAYS[gateway]
|
|
92
|
+
runtime.convex_url = (convex_url or default_convex_url).strip().rstrip("/")
|
|
93
|
+
runtime.web_url = (web_url or default_web_url).strip().rstrip("/")
|
|
94
|
+
if ctx.invoked_subcommand is None and not runtime.in_repl:
|
|
95
|
+
run_repl(ctx, runtime)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@cli.command()
|
|
99
|
+
@json_option
|
|
100
|
+
@pass_runtime
|
|
101
|
+
def skill(runtime: Runtime) -> None:
|
|
102
|
+
"""Print the skill that teaches a coding agent the setup flow."""
|
|
103
|
+
text = files(__package__).joinpath("skills/SKILL.md").read_text(encoding="utf-8")
|
|
104
|
+
emit(runtime.json, {"skill": text}, text.rstrip("\n"))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
CLI_PROVIDER = "cli"
|
|
108
|
+
|
|
109
|
+
SIGN_IN_REFUSED = (
|
|
110
|
+
"The approval was refused: it expired, was used, or you are no longer an Admin."
|
|
111
|
+
" Run login again."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _viewer(runtime: Runtime) -> dict:
|
|
116
|
+
viewer = runtime.client().query("users:viewer")
|
|
117
|
+
if not viewer:
|
|
118
|
+
raise signed_out()
|
|
119
|
+
return {
|
|
120
|
+
"email": viewer["email"],
|
|
121
|
+
"name": viewer["name"],
|
|
122
|
+
"gateway": runtime.convex_url,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@cli.command()
|
|
127
|
+
@json_option
|
|
128
|
+
@pass_runtime
|
|
129
|
+
def login(runtime: Runtime) -> None:
|
|
130
|
+
"""Sign in: approve in the browser with your Admin account."""
|
|
131
|
+
state = new_secret()
|
|
132
|
+
verifier = new_secret()
|
|
133
|
+
url = sign_in_url(
|
|
134
|
+
runtime.web_url, state, pkce_challenge(verifier), runtime.hostname()
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def open_page() -> None:
|
|
138
|
+
click.echo(f"Open this page to approve: {url}", err=True)
|
|
139
|
+
try:
|
|
140
|
+
runtime.open_browser(url)
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
code = wait_for_code(runtime.callback_port, state, runtime.login_timeout, open_page)
|
|
145
|
+
gateway = runtime.gateway
|
|
146
|
+
gateway.set_auth(None)
|
|
147
|
+
result = gateway.action(
|
|
148
|
+
"auth:signIn",
|
|
149
|
+
{"provider": CLI_PROVIDER, "params": {"code": code, "codeVerifier": verifier}},
|
|
150
|
+
)
|
|
151
|
+
tokens = (result or {}).get("tokens")
|
|
152
|
+
if not tokens:
|
|
153
|
+
raise CliError("sign_in_refused", SIGN_IN_REFUSED)
|
|
154
|
+
runtime.store.put(runtime.convex_url, tokens["token"], tokens["refreshToken"])
|
|
155
|
+
viewer = _viewer(runtime)
|
|
156
|
+
emit(runtime.json, viewer, f"Signed in as {viewer['email']} on {viewer['gateway']}")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@cli.command()
|
|
160
|
+
@json_option
|
|
161
|
+
@pass_runtime
|
|
162
|
+
def logout(runtime: Runtime) -> None:
|
|
163
|
+
"""Sign out: end this gateway's session and forget it."""
|
|
164
|
+
if runtime.store.get(runtime.convex_url) is not None:
|
|
165
|
+
try:
|
|
166
|
+
runtime.client().action("auth:signOut")
|
|
167
|
+
except CliError:
|
|
168
|
+
pass
|
|
169
|
+
runtime.store.drop(runtime.convex_url)
|
|
170
|
+
emit(
|
|
171
|
+
runtime.json,
|
|
172
|
+
{"signedOut": True, "gateway": runtime.convex_url},
|
|
173
|
+
f"Signed out of {runtime.convex_url}",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@cli.command()
|
|
178
|
+
@json_option
|
|
179
|
+
@pass_runtime
|
|
180
|
+
def whoami(runtime: Runtime) -> None:
|
|
181
|
+
"""Say who is signed in, and on which gateway."""
|
|
182
|
+
viewer = _viewer(runtime)
|
|
183
|
+
emit(runtime.json, viewer, f"{viewer['email']} on {viewer['gateway']}")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
cli.add_command(apps)
|
|
187
|
+
cli.add_command(servers)
|
|
188
|
+
cli.add_command(methods)
|
|
189
|
+
cli.add_command(webhooks)
|
|
190
|
+
cli.add_command(payments)
|
|
191
|
+
cli.add_command(go_live)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def main() -> None:
|
|
195
|
+
cli(obj=Runtime())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The setup commands, one module per group."""
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""`apps`: find the app this project belongs to, create it, brand it."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from ..core.client import Client
|
|
10
|
+
from ..core.config import ENVIRONMENTS, LIVE, TEST
|
|
11
|
+
from ..core.errors import CliError
|
|
12
|
+
from ..core.runtime import Runtime
|
|
13
|
+
from ..utils.options import json_option, pass_runtime
|
|
14
|
+
from ..utils.output import emit, server_lines
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.group()
|
|
18
|
+
def apps() -> None:
|
|
19
|
+
"""List, read, create and brand apps."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _state(app: dict) -> str:
|
|
23
|
+
state = LIVE if app["live"] else TEST
|
|
24
|
+
return f"{state}, archived" if app["archived"] else state
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@apps.command("list")
|
|
28
|
+
@json_option
|
|
29
|
+
@pass_runtime
|
|
30
|
+
def list_apps(runtime: Runtime) -> None:
|
|
31
|
+
"""Every app, archived ones last."""
|
|
32
|
+
found = runtime.client().query("apps/queries:list")
|
|
33
|
+
lines = [f"{app['slug']} {app['name']} {_state(app)}" for app in found]
|
|
34
|
+
emit(runtime.json, found, "\n".join(lines) or "No app yet.")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def app_detail(runtime: Runtime, slug: str) -> dict:
|
|
38
|
+
"""The app with its Test and Live sides, or a `not_found` error."""
|
|
39
|
+
detail = runtime.client().query("apps/queries:get", {"slug": slug})
|
|
40
|
+
if detail is None:
|
|
41
|
+
raise CliError("not_found", f"No app {slug}. Run: apps list")
|
|
42
|
+
return detail
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@apps.command()
|
|
46
|
+
@click.argument("slug")
|
|
47
|
+
@json_option
|
|
48
|
+
@pass_runtime
|
|
49
|
+
def get(runtime: Runtime, slug: str) -> None:
|
|
50
|
+
"""One app with its servers, URLs and key hints, in Test and Live."""
|
|
51
|
+
detail = app_detail(runtime, slug)
|
|
52
|
+
app = detail["app"]
|
|
53
|
+
lines = [
|
|
54
|
+
(
|
|
55
|
+
f"{app['name']} ({app['slug']}): {_state(app)},"
|
|
56
|
+
f" colour {app['color']}, language {app['defaultLanguage']}"
|
|
57
|
+
)
|
|
58
|
+
]
|
|
59
|
+
for side in ENVIRONMENTS:
|
|
60
|
+
if detail[side]:
|
|
61
|
+
lines.append(f"{side.capitalize()}:")
|
|
62
|
+
for server in detail[side]["servers"]:
|
|
63
|
+
lines.extend(server_lines(server))
|
|
64
|
+
emit(runtime.json, detail, "\n".join(lines))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
LANGUAGE = click.Choice(["fr", "en"])
|
|
68
|
+
LOGO = click.Path(dir_okay=False, path_type=Path)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def upload_logo(client: Client, logo: Path) -> str:
|
|
72
|
+
"""The logo file in Convex storage; returns its storage id."""
|
|
73
|
+
upload_url = client.mutation("apps/mutations:generateLogoUploadUrl")
|
|
74
|
+
return client.upload(upload_url, logo)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@apps.command()
|
|
78
|
+
@click.option("--name", required=True, help="As payers see it, e.g. Heyama.")
|
|
79
|
+
@click.option("--color", required=True, help="Brand colour, e.g. #0F7B5F.")
|
|
80
|
+
@click.option(
|
|
81
|
+
"--language", required=True, type=LANGUAGE, help="Checkout's default language."
|
|
82
|
+
)
|
|
83
|
+
@click.option("--logo", type=LOGO, help="Logo file, e.g. public/logo.png.")
|
|
84
|
+
@json_option
|
|
85
|
+
@pass_runtime
|
|
86
|
+
def create(
|
|
87
|
+
runtime: Runtime, name: str, color: str, language: str, logo: Path | None
|
|
88
|
+
) -> None:
|
|
89
|
+
"""A new app, in Test with a Dev server and no key yet."""
|
|
90
|
+
client = runtime.client()
|
|
91
|
+
args = {"name": name.strip(), "color": color.strip(), "defaultLanguage": language}
|
|
92
|
+
if logo is not None:
|
|
93
|
+
args["logoId"] = upload_logo(client, logo)
|
|
94
|
+
slug = client.mutation("apps/mutations:create", args)["slug"]
|
|
95
|
+
emit(
|
|
96
|
+
runtime.json,
|
|
97
|
+
{"slug": slug},
|
|
98
|
+
f"Created {slug}, in Test with a Dev server and no key yet. Next: apps get {slug}",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
#: What `updateBranding` would clear if left out: sent back as they are.
|
|
103
|
+
KEPT = ("termsUrl", "privacyUrl", "supportEmail", "supportWhatsapp")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@apps.command()
|
|
107
|
+
@click.argument("slug")
|
|
108
|
+
@click.option("--name", help="As payers see it, e.g. Heyama.")
|
|
109
|
+
@click.option("--color", help="Brand colour, e.g. #0F7B5F.")
|
|
110
|
+
@click.option("--language", type=LANGUAGE, help="Checkout's default language.")
|
|
111
|
+
@click.option("--logo", type=LOGO, help="Logo file, e.g. public/logo.png.")
|
|
112
|
+
@json_option
|
|
113
|
+
@pass_runtime
|
|
114
|
+
def branding(
|
|
115
|
+
runtime: Runtime,
|
|
116
|
+
slug: str,
|
|
117
|
+
name: str | None,
|
|
118
|
+
color: str | None,
|
|
119
|
+
language: str | None,
|
|
120
|
+
logo: Path | None,
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Change an app's name, colour, language or logo; the rest stays."""
|
|
123
|
+
if name is None and color is None and language is None and logo is None:
|
|
124
|
+
raise CliError(
|
|
125
|
+
"nothing_to_change",
|
|
126
|
+
"Give at least one of --name, --color, --language, --logo.",
|
|
127
|
+
)
|
|
128
|
+
client = runtime.client()
|
|
129
|
+
app = app_detail(runtime, slug)["app"]
|
|
130
|
+
args = {
|
|
131
|
+
"slug": slug,
|
|
132
|
+
"name": app["name"] if name is None else name.strip(),
|
|
133
|
+
"color": app["color"] if color is None else color.strip(),
|
|
134
|
+
"defaultLanguage": language or app["defaultLanguage"],
|
|
135
|
+
**{field: app[field] for field in KEPT if app[field] is not None},
|
|
136
|
+
}
|
|
137
|
+
if logo is not None:
|
|
138
|
+
args["logoId"] = upload_logo(client, logo)
|
|
139
|
+
client.mutation("apps/mutations:updateBranding", args)
|
|
140
|
+
emit(
|
|
141
|
+
runtime.json,
|
|
142
|
+
{"slug": slug, "updated": True},
|
|
143
|
+
f"Updated the branding of {slug}.",
|
|
144
|
+
)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""`go-live`: the app takes real payments. Only after the Admin's yes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ..core.config import LIVE
|
|
8
|
+
from ..core.errors import CliError
|
|
9
|
+
from ..core.runtime import Runtime
|
|
10
|
+
from ..utils.options import json_option, pass_runtime
|
|
11
|
+
from ..utils.output import emit
|
|
12
|
+
from .apps import app_detail
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def production_server_id(detail: dict) -> str:
|
|
16
|
+
"""The id of a live app's Production server."""
|
|
17
|
+
return detail[LIVE]["servers"][0]["id"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@click.command("go-live")
|
|
21
|
+
@click.argument("slug")
|
|
22
|
+
@click.option(
|
|
23
|
+
"--webhook-url",
|
|
24
|
+
help="Production webhook URL, e.g. https://heyama.com/api/webhooks.",
|
|
25
|
+
)
|
|
26
|
+
@click.option(
|
|
27
|
+
"--return-url",
|
|
28
|
+
"return_urls",
|
|
29
|
+
multiple=True,
|
|
30
|
+
help="Production return URL, e.g. https://heyama.com. Repeat for more.",
|
|
31
|
+
)
|
|
32
|
+
@click.option("--yes", is_flag=True, help="Go live: the Admin agreed.")
|
|
33
|
+
@json_option
|
|
34
|
+
@pass_runtime
|
|
35
|
+
def go_live(
|
|
36
|
+
runtime: Runtime,
|
|
37
|
+
slug: str,
|
|
38
|
+
webhook_url: str | None,
|
|
39
|
+
return_urls: tuple[str, ...],
|
|
40
|
+
yes: bool,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Take a Test app live at its Production URLs. Needs --yes."""
|
|
43
|
+
detail = app_detail(runtime, slug)
|
|
44
|
+
if detail[LIVE]:
|
|
45
|
+
raise CliError(
|
|
46
|
+
"already_live",
|
|
47
|
+
f"{slug} is already live. Change its Production URLs with"
|
|
48
|
+
f" servers update {production_server_id(detail)}.",
|
|
49
|
+
)
|
|
50
|
+
webhook = webhook_url.strip() if webhook_url else None
|
|
51
|
+
returns = [url.strip() for url in return_urls]
|
|
52
|
+
args: dict = {"slug": slug, "returnUrls": returns}
|
|
53
|
+
if webhook:
|
|
54
|
+
args["webhookUrl"] = webhook
|
|
55
|
+
client = runtime.client()
|
|
56
|
+
if not yes:
|
|
57
|
+
# Every check of going live, nothing written: a refusal comes before the summary.
|
|
58
|
+
client.mutation("apps/mutations:goLive", {**args, "dryRun": True})
|
|
59
|
+
at = f"webhook URL {webhook}" if webhook else "no webhook URL"
|
|
60
|
+
raise CliError(
|
|
61
|
+
"confirmation_required",
|
|
62
|
+
f"Going live makes {slug} take real payments, with the methods and"
|
|
63
|
+
f" settings it has in Test, on a Production server with {at} and"
|
|
64
|
+
f" return URLs {', '.join(returns) or 'none'}. Its live API key and"
|
|
65
|
+
" webhook secret are shown once. Ask the Admin, then run again with --yes.",
|
|
66
|
+
)
|
|
67
|
+
secrets = client.mutation("apps/mutations:goLive", args)
|
|
68
|
+
server_id = production_server_id(app_detail(runtime, slug))
|
|
69
|
+
emit(
|
|
70
|
+
runtime.json,
|
|
71
|
+
{"slug": slug, "serverId": server_id, **secrets},
|
|
72
|
+
f"{slug} is live. Production server {server_id}, shown this once:\n"
|
|
73
|
+
f"API key: {secrets['apiKey']}\n"
|
|
74
|
+
f"Webhook secret: {secrets['webhookSecret']}",
|
|
75
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""`methods`: the payment methods, and which ones an app offers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ..core.config import TEST
|
|
8
|
+
from ..core.runtime import Runtime
|
|
9
|
+
from ..utils.options import environment_option, json_option, pass_runtime
|
|
10
|
+
from ..utils.output import emit
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group()
|
|
14
|
+
def methods() -> None:
|
|
15
|
+
"""List the payment methods; turn one on or off for an app."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@methods.command("list")
|
|
19
|
+
@environment_option
|
|
20
|
+
@click.option("--app", "slug", help="Say whether this app offers each, e.g. heyama.")
|
|
21
|
+
@json_option
|
|
22
|
+
@pass_runtime
|
|
23
|
+
def list_methods(runtime: Runtime, environment: str, slug: str | None) -> None:
|
|
24
|
+
"""Every method in checkout order, with its provider and status."""
|
|
25
|
+
found = runtime.client().query("methods/queries:list", {"environment": environment})
|
|
26
|
+
lines = []
|
|
27
|
+
for row in found:
|
|
28
|
+
served = "by " + row["servedBy"]["name"] if row["servedBy"] else "no provider"
|
|
29
|
+
line = (
|
|
30
|
+
f"{row['id']} {row['name']} {row['country']} {row['currency']}"
|
|
31
|
+
f" {row['status']}, {served}"
|
|
32
|
+
)
|
|
33
|
+
if slug is not None:
|
|
34
|
+
row["on"] = slug not in row["offApps"]
|
|
35
|
+
line += f" {'on' if row['on'] else 'off'} for {slug}"
|
|
36
|
+
lines.append(line)
|
|
37
|
+
emit(runtime.json, found, "\n".join(lines))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@methods.command("set")
|
|
41
|
+
@click.argument("slug")
|
|
42
|
+
@click.argument("method")
|
|
43
|
+
@click.option("--on/--off", "on", default=None, help="Offer it, or not.")
|
|
44
|
+
@json_option
|
|
45
|
+
@pass_runtime
|
|
46
|
+
def set_method(runtime: Runtime, slug: str, method: str, on: bool | None) -> None:
|
|
47
|
+
"""Let the app offer a method at checkout in Test, or not. Live follows at go live."""
|
|
48
|
+
if on is None:
|
|
49
|
+
raise click.UsageError("Give --on or --off.")
|
|
50
|
+
runtime.client().mutation(
|
|
51
|
+
"methods/mutations:setAppAccess",
|
|
52
|
+
{"method": method, "environment": TEST, "slug": slug, "enabled": on},
|
|
53
|
+
)
|
|
54
|
+
emit(
|
|
55
|
+
runtime.json,
|
|
56
|
+
{"slug": slug, "method": method, "environment": TEST, "on": on},
|
|
57
|
+
f"{method} is {'on' if on else 'off'} for {slug} in {TEST}.",
|
|
58
|
+
)
|