Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ out of reach.
| notifications/sms | 34/34 | every endpoint — see `docs/sms.md`; each command is run through the real transport in `tests/sms-routes.test.ts` |
| scheduler | 8/8 | every endpoint, `save` included (email-campaign task built from flags) — see `docs/scheduler.md`; each command is run through the real transport in `tests/scheduler-routes.test.ts`, with the exact `save` body |
| webhooks | 6/9 | show, secret, rotate, enable/disable/remove destination |
| payments | 7/16 | integrations list, triggers list/get/enable/disable, module toggle |
| payments | 17/17 | every endpoint a person calls (the inbound provider webhook is called by the provider) — see `docs/payments.md`; each command is run through the real transport in `tests/payments-routes.test.ts` |
| account/* | 12/64 | profile, status, usage, projects, team (paging + project / owner filters), regions, billing-portal, api keys, me, me set-phone — see `docs/account.md`; team / me / set-phone run through the real transport in `tests/account-routes.test.ts`. Missing: team roles/policies management |
| account/projects (settings, CORS, admin portal, legal, AI chat) | 30/30 | every project route in `@norbix.ai/ts` 4.4.0 a developer calls (28 hub + the 2 public API-host reads) — see `docs/project.md`; each command is run through the real transport in `tests/project-routes.test.ts`. Not counted: create project, environments, notification groups/tags, AI plans/knowledge/credits (internal), expose brand/auth (next wave) |
| ai/integrations (LLM, MCP) | 15/20 | every LLM and MCP route — see `docs/ai.md`, same route test. Missing: the 5 embedding-integration routes (not asked for; `norbix hub ai …` reaches them) |
Expand All @@ -54,7 +54,7 @@ Cross-cutting commands: `norbix integrations <module>` lists integrations for
— signup/onboarding flows that happen in the portal or email links.
- **webhooks/module receive-webhook** — inbound endpoint called by external
services, not by a person.
- **save/config of integrations** (database, email, files, logging, payments,
- **save/config of integrations** (database, email, files, logging,
code, membership, AI) — large nested JSON configs; the portal validates
them much better. Readable via `norbix integrations <module>`; writable via
`norbix api` if ever needed.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ norbix scheduler list
| `norbix account profile/status/usage/projects/team/regions/billing-portal/me` | Account-level info; `account team` pages and filters, `account me set-phone` saves the phone "Account users" SMS campaigns use — see [docs/account.md](docs/account.md) |
| `norbix project ...` | One project's settings — name, look, languages, regions, CORS origins, admin portal, legal documents, AI chat and assistants, see [docs/project.md](docs/project.md) |
| `norbix ai ...` | LLM and MCP server integrations, AI service users and their keys, see [docs/ai.md](docs/ai.md) |
| `norbix payments integrations/triggers/trigger/enable/disable` | Payment integrations and triggers |
| `norbix payments ...` | Payments module: provider integrations (Stripe, Apple In-App, Google In-App, …), the received-webhook log, payment triggers — every Payments endpoint a person calls, see [docs/payments.md](docs/payments.md) |
| `norbix code functions/qualifiers/invoke/event-sample/runs` | Cloud functions (AWS Lambda, Azure Functions, Google Cloud Functions): list what a code integration may call, aliases and versions, call one by hand, a sample envelope for a local run, the last runs of a trigger or task — see [docs/code.md](docs/code.md) |
| `norbix code marketplace listings/connections/functions/runs` | Marketplace: browse listings, enable a listing as a connection, validate it, sign in to an MCP connection (OAuth) and out again, list an MCP connection's tools, set who may use it (access matrix, a role's package, a custom policy), add functions (db.find, chat.postMessage, an MCP tool, …), call one by hand, the last runs of a function, trigger, task or an MCP connection's AI tool calls (`--details`: input, steps, answer when the project's logs are on) — see [docs/code.md](docs/code.md#norbix-code-marketplace) |
| `norbix integrations <module>` | List integrations of any module |
Expand Down
125 changes: 125 additions & 0 deletions docs/payments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# `norbix payments`

Manage the Payments module from the terminal. Norbix does not take payments:
it **receives the webhooks** your payment provider sends (Stripe, Apple
In-App, Google In-App, …), checks their signature, turns each one into one
event — `payments.webhook.received` — and runs your **payment triggers** on
it: send an email, SMS or push, call a webhook, run a function.

Every endpoint a person calls has a command. Every command is covered by
`tests/payments-routes.test.ts`, which runs it through the real SDK with
`fetch` replaced and checks the verb and path it sends, plus the request
fields of `save`, `test`, `webhook-log` and the toggles. No test contacts a
payment provider.

All paths below are under `/{version}/payments`.

## Module

| command | what it does | endpoint |
|---|---|---|
| `norbix payments enable` | turn the Payments module on | `PUT /enable` |
| `norbix payments disable [--yes]` | turn the Payments module off | `PUT /disable` |
| `norbix payments webhook-log [--integration <id>] [--limit <n>]` | the provider webhooks the project received, newest first | `GET /webhooks/log` |

`norbix module enable payments` / `norbix module disable payments` do the
same as the first two, for scripts that treat every module alike.

**Changed in 1.34:** up to 1.33, `payments enable <id>` and
`payments disable <id>` turned a *trigger* on or off. They now turn the
module on or off, like `sms enable` and `email enable`; the trigger toggles
are `payments trigger enable|disable <id>`. The old form fails with exit 2,
sends nothing, and its hint names the new command.

`webhook-log` shows one row per provider call: the integration, the provider,
the env, the event name and the provider's event ID, the HTTP status Norbix
answered (200 processed, 400 bad signature, 404 unknown integration, …) with
a description, and when it arrived. Look here first when a trigger did not
fire: no row means the provider never reached Norbix; a 400 usually means the
signing secret in the integration is wrong.

## Integrations

| command | what it does | endpoint |
|---|---|---|
| `norbix payments integrations` | list the integrations | `GET /integrations` |
| `norbix payments integration <id>` | show one integration | `GET /integrations/{id}` |
| `norbix payments integration save --provider <p> [--name <n>] [--id <id>] [--disabled] [--config <json>]` | create or update an integration | `POST /integrations` |
| `norbix payments integration test --integration <id>` | check the credentials with the provider | `POST /integrations/test` |
| `norbix payments integration confirm-delivery <id>` | confirm you checked the test result | `POST /integrations/confirm-human-delivery` |
| `norbix payments integration enable <id>` | turn it on | `PUT /integrations/{Id}/enable` |
| `norbix payments integration disable <id> [--yes]` | turn it off | `PUT /integrations/{Id}/disable` |
| `norbix payments integration delete <id> [--yes]` | delete it | `DELETE /integrations/{Id}` |

`--provider` is one of `Stripe`, `AppleInApp`, `GoogleInApp` (fully
supported), or `PayPal`, `Adyen`, `Paddle`, `LemonSqueezy`, `Mollie`,
`Shopify`, `WooCommerce`, `Magento`, `Braintree`, `AuthorizeNet`,
`CheckOutCom`, `Worldpay`. Put the provider-specific fields in `--config` as
a JSON object (inline, `@file.json`, or `-` for stdin). For Stripe:

```json
{"secretKey": "sk_test_…", "publishableKey": "pk_test_…", "webhookSigningSecret": "whsec_…"}
```

Use test-mode keys while you develop. `webhookSigningSecret` is the secret of
the Stripe webhook endpoint that points at Norbix. The portal shows that
endpoint's URL on the integration; its shape is
`POST /{version}/payments/webhooks/{accountId}/{projectId}/{provider}/{integrationId}`
(plus `?env=TEST` for a non-production copy of the integration).

```bash
norbix payments integration save --provider Stripe --name Stripe --config @stripe.json
norbix payments integration test --integration int_123
norbix payments integration confirm-delivery int_123
```

## Triggers

| command | what it does | endpoint |
|---|---|---|
| `norbix payments triggers` | list the payment triggers | `GET /triggers` |
| `norbix payments trigger <id>` | show one trigger | `GET /triggers/{id}` |
| `norbix payments trigger save --file <json> [--id <id>] [--event <name>]... [--integration <id>]... [--order <n>] [--[no-]break-on-error]` | create or update a trigger | `POST /triggers` |
| `norbix payments trigger enable <id>` | turn it on | `PATCH /triggers/{triggerId}/enable` |
| `norbix payments trigger disable <id> [--yes]` | turn it off | `PATCH /triggers/{triggerId}/disable` |
| `norbix payments trigger delete <id> [--yes]` | delete it | `DELETE /triggers/{triggerId}` |

The `--file` holds the trigger — the same shape `payments trigger <id> --json`
shows. `type` (`Payments`) and `when` (`OnWebhookCallReceived`, the only one
that fires) are filled in when the file leaves them out.

- `events` — the provider's own event names, e.g. `payment_intent.succeeded`
or `charge.refunded`. Empty = every event in the server's list for that
provider. A name the server does not know is refused.
- `integrations` — integration IDs. Empty = every payments integration.
- `action` — what to run: `{"type": "WebhookCall"}`, `Email`, `Sms`, `Push`,
`Code`, `SseCall` or `Marketplace`, each with its own fields (the portal's
trigger page builds them).

`--event` and `--integration` (repeat for several) replace the file's lists.
`--order` and `--break-on-error` set the trigger's place in the queue of the
same event, as for `db trigger create`.

```json
{"name": "Payment received", "events": ["payment_intent.succeeded"], "action": {"type": "WebhookCall"}}
```

```bash
norbix payments trigger save --file paid.json
norbix payments trigger save --file paid.json --event payment_intent.succeeded --event invoice.paid
norbix payments trigger disable trg_123 --yes
```

Templates of a trigger's email, SMS or push read the payment as
`@Model.Payment.*` tokens (`Payment.EventName`, `Payment.Amount`,
`Payment.Currency`, …). A function gets the payment as `data.payment` of
the `payments.webhook.received` event. A `WebhookCall` action sends the
`payments.webhook.received` event, with the payment as its data, to the
project's webhook destinations that listen to it (`norbix webhooks show`).

## Not offered on purpose

- The inbound webhook (`POST /payments/webhooks/{accountId}/…`) — the payment
provider calls it, not a person.
- Orders, payment schemas, customers, plans and discounts — the Payments
module is a webhook receiver in this phase; those parts are not live.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
},
"dependencies": {
"@inquirer/prompts": "^8.7.2",
"@norbix.ai/ts": "^4.26.0",
"@norbix.ai/ts": "^4.28.0",
"@oclif/core": "^5.0.0",
"@oclif/plugin-autocomplete": "^4.0.0",
"@oclif/plugin-help": "^7.0.0"
Expand Down
2 changes: 0 additions & 2 deletions scripts/gen-commands.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,6 @@ const files = {
'src/commands/payments/integrations.ts': list('PaymentsIntegrations', 'List payment integrations', 'client.hub.payments.getPaymentsIntegrations', {example: 'payments integrations'}),
'src/commands/payments/triggers.ts': list('PaymentsTriggers', 'List payment triggers', 'client.hub.payments.getPaymentsTriggers', {example: 'payments triggers'}),
'src/commands/payments/trigger.ts': getById('PaymentsTrigger', 'Show one payment trigger', 'client.hub.payments.getPaymentsTrigger', {example: 'payments trigger 66b2f0a1...', argDesc: 'Trigger ID'}),
'src/commands/payments/enable.ts': action('PaymentsEnable', 'Enable a payment trigger', 'client.hub.payments.enablePaymentsTrigger', 'Trigger \${args.id} enabled.', {example: 'payments enable 66b2f0a1...', argDesc: 'Trigger ID', idField: 'triggerId'}),
'src/commands/payments/disable.ts': action('PaymentsDisable', 'Disable a payment trigger', 'client.hub.payments.disablePaymentsTrigger', 'Trigger \${args.id} disabled.', {example: 'payments disable 66b2f0a1...', argDesc: 'Trigger ID', idField: 'triggerId'}),
}

for (const [path, content] of Object.entries(files)) {
Expand Down
11 changes: 8 additions & 3 deletions scripts/gen-request-fields.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ function docBefore(block, at) {
export function methodsOf(block, label = 'module') {
const methods = {}
const problems = []
const forwarders = new Set()
let sendsInMethods = 0
for (const m of block.matchAll(SIGNATURE_RE)) {
const name = m[1]
Expand Down Expand Up @@ -113,14 +114,18 @@ export function methodsOf(block, label = 'module') {
}

methods[name] = {http, path, dto: dtos[0], target}
// A deprecated forwarder may call the other host's route (4.28.0:
// hub.notifications.grantContactConsent → the API route); the host check
// below lets it through.
if (/@deprecated\b/.test(doc)) forwarders.add(name)
}

const sends = block.split(SEND).length - 1
if (sends !== sendsInMethods) {
problems.push(`${label}: ${sends - sendsInMethods} send call(s) outside a method of the shape \`name = (request = {}, options = {}) => {\``)
}

return {methods, problems}
return {methods, problems, forwarders}
}

/** `this.membership = new MembershipModule(transport)` inside a namespace class. */
Expand Down Expand Up @@ -235,10 +240,10 @@ export function sdkMethods(indexJs) {
continue
}

const {methods, problems: p} = methodsOf(block, `${target}.${moduleName}`)
const {methods, problems: p, forwarders} = methodsOf(block, `${target}.${moduleName}`)
problems.push(...p)
for (const [name, info] of Object.entries(methods)) {
if (info.target !== target) problems.push(`${target}.${moduleName}.${name}: sends to "${info.target}", not "${target}"`)
if (info.target !== target && !forwarders.has(name)) problems.push(`${target}.${moduleName}.${name}: sends to "${info.target}", not "${target}"`)
}

result[target][moduleName] = methods
Expand Down
25 changes: 12 additions & 13 deletions src/commands/payments/disable.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
import {Args} from '@oclif/core'

import {BaseCommand} from '../../base.js'

export default class PaymentsDisable extends BaseCommand {
static description = 'Disable a payment trigger'
static description = `Turn off the Payments module for the project

Incoming provider webhooks stop firing triggers; the integrations, triggers
and the webhook log are kept. Up to 1.33 this command took a trigger ID —
that is now \`payments trigger disable <id>\`.`

static examples = [
'<%= config.bin %> payments disable 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run',
'<%= config.bin %> payments disable 66b2f0a1c3d4e5f6a7b8c9d0 --yes',
'<%= config.bin %> payments disable --yes',
'<%= config.bin %> payments disable --dry-run',
]

static args = {
id: Args.string({required: true, description: 'Trigger ID'}),
}

static flags = {
...BaseCommand.mutatingFlags,
}

async run(): Promise<unknown> {
const {args, flags} = await this.parse(PaymentsDisable)
const {flags} = await this.parse(PaymentsDisable)
const client = this.client(flags)
await this.confirmOrFail(`Disable payment trigger ${args.id}?`, flags)
await this.confirmOrFail('Turn off the Payments module?', flags)

const res = await client.hub.payments.disablePayments({})

const res = await client.hub.payments.disablePaymentsTrigger({triggerId: args.id})
this.print(`Trigger ${args.id} disabled.`)
this.print('Payments disabled.')
return res
}
}
22 changes: 10 additions & 12 deletions src/commands/payments/enable.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,27 @@
import {Args} from '@oclif/core'

import {BaseCommand} from '../../base.js'

export default class PaymentsEnable extends BaseCommand {
static description = 'Enable a payment trigger'
static description = `Turn on the Payments module for the project

Up to 1.33 this command took a trigger ID and turned a payment trigger on —
that is now \`payments trigger enable <id>\`.`

static examples = [
'<%= config.bin %> payments enable 66b2f0a1c3d4e5f6a7b8c9d0',
'<%= config.bin %> payments enable 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run',
'<%= config.bin %> payments enable',
'<%= config.bin %> payments enable --dry-run',
]

static args = {
id: Args.string({required: true, description: 'Trigger ID'}),
}

static flags = {
...BaseCommand.dryRunFlags,
}

async run(): Promise<unknown> {
const {args, flags} = await this.parse(PaymentsEnable)
const {flags} = await this.parse(PaymentsEnable)
const client = this.client(flags)

const res = await client.hub.payments.enablePaymentsTrigger({triggerId: args.id})
this.print(`Trigger ${args.id} enabled.`)
const res = await client.hub.payments.enablePayments({})

this.print('Payments enabled.')
return res
}
}
23 changes: 23 additions & 0 deletions src/commands/payments/integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {Args} from '@oclif/core'

import {BaseCommand} from '../../base.js'

export default class PaymentsIntegration extends BaseCommand {
static description = 'Show one payments integration'

static examples = ['<%= config.bin %> payments integration 66b2f0a1c3d4e5f6a7b8c9d0']

static args = {
id: Args.string({required: true, description: 'Integration ID'}),
}

async run(): Promise<unknown> {
const {args, flags} = await this.parse(PaymentsIntegration)
const client = this.client(flags)

const res = await client.hub.payments.getPaymentsIntegration({id: args.id})

this.print(res)
return res
}
}
34 changes: 34 additions & 0 deletions src/commands/payments/integration/confirm-delivery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {Args} from '@oclif/core'

import {BaseCommand} from '../../../base.js'

export default class PaymentsIntegrationConfirmDelivery extends BaseCommand {
static description = `Confirm that you checked a payments integration's test result

After \`payments integration test\`, run this once you have seen the result
(for example the account in the provider's dashboard). It marks the
integration as proven to work.`

static examples = [
'<%= config.bin %> payments integration confirm-delivery 66b2f0a1c3d4e5f6a7b8c9d0',
'<%= config.bin %> payments integration confirm-delivery 66b2f0a1c3d4e5f6a7b8c9d0 --dry-run',
]

static args = {
id: Args.string({required: true, description: 'Integration ID'}),
}

static flags = {
...BaseCommand.dryRunFlags,
}

async run(): Promise<unknown> {
const {args, flags} = await this.parse(PaymentsIntegrationConfirmDelivery)
const client = this.client(flags)

const res = await client.hub.payments.confirmPaymentsIntegrationHumanDelivery({integrationId: args.id})

this.print(`Test result confirmed for integration ${args.id}.`)
return res
}
}
Loading
Loading