@jskit-ai/payments-core 0.1.1
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.
- package/README.md +147 -0
- package/contracts/configuration.schema.json +148 -0
- package/contracts/conformance.json +718 -0
- package/docs/conformance.md +91 -0
- package/docs/contract.md +126 -0
- package/docs/standalone.md +259 -0
- package/migrations/payments_core_initial.cjs +26 -0
- package/package.json +68 -0
- package/src/server/catalogue.js +117 -0
- package/src/server/checkout.js +131 -0
- package/src/server/knexStore.js +82 -0
- package/src/server/paddle.js +117 -0
- package/src/server/readiness.js +63 -0
- package/src/server/service.js +160 -0
- package/src/server/stripe.js +138 -0
- package/src/shared/configuration.js +40 -0
- package/test/catalogue.test.js +112 -0
- package/test/checkout.test.js +229 -0
- package/test/conformance.test.js +77 -0
- package/test/history.test.js +105 -0
- package/test/readiness.test.js +55 -0
- package/test/service.test.js +118 -0
- package/test/standalone.test.js +61 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Portable payment examples and conformance
|
|
2
|
+
|
|
3
|
+
`contracts/conformance.json` is ordinary JSON, also exported as
|
|
4
|
+
`@jskit-ai/payments-core/conformance.json`. Read the file as data; no Node process,
|
|
5
|
+
JSKIT import, editor service or Genesis command is necessary to interpret it.
|
|
6
|
+
It accompanies the draft-07 configuration schema and the semantic contract.
|
|
7
|
+
These are test examples, not a production workflow interpreter.
|
|
8
|
+
|
|
9
|
+
## Configuration examples
|
|
10
|
+
|
|
11
|
+
Each `configurationCases` entry supplies a complete payment-relevant document
|
|
12
|
+
and two expected booleans:
|
|
13
|
+
|
|
14
|
+
- `schemaValid`: validate `document.extensions.payments` against
|
|
15
|
+
`configuration.schema.json` using a draft-07 validator, without coercing values,
|
|
16
|
+
injecting defaults or removing unknown properties.
|
|
17
|
+
- `configurationValid`: after that validation, apply the connection references
|
|
18
|
+
below. This does not replace full integration-document validation.
|
|
19
|
+
|
|
20
|
+
For each named payment environment:
|
|
21
|
+
|
|
22
|
+
1. Resolve `integrationId` from `document.integrations`; absence fails.
|
|
23
|
+
2. Require provider `stripe` or `paddle`, `accountMode` equal to `shared`, and
|
|
24
|
+
`authentication.method` equal to `api-key`.
|
|
25
|
+
3. Require `authentication.secretRef` to match `env:[A-Z_][A-Z0-9_]*` in its
|
|
26
|
+
entirety. This describes a reference; never read or copy a real secret into
|
|
27
|
+
the test fixture or source configuration.
|
|
28
|
+
4. For Paddle, require the integration's `settings.environment` to equal the
|
|
29
|
+
payment environment, and require `taxCategory` and `publicClientTokenRef` in
|
|
30
|
+
the payment binding. Their values already passed the payment schema.
|
|
31
|
+
|
|
32
|
+
The examples distinguish malformed prices/credits/features and inline secrets
|
|
33
|
+
from structurally valid but unresolved or mismatched merchant connections.
|
|
34
|
+
The fixture credentials are references only. Passing validation does not prove
|
|
35
|
+
that an Env value exists, that a merchant owns a key, or that it can take payments.
|
|
36
|
+
Resolve and verify those resources separately at application setup.
|
|
37
|
+
|
|
38
|
+
## Account behavior sequence
|
|
39
|
+
|
|
40
|
+
`accountScenario` supplies the payment-relevant configuration, merchant and
|
|
41
|
+
subject scope, initial UTC clock in milliseconds, and a provider customer to
|
|
42
|
+
bind to that subject. Start with a fresh transactional fixture database. Process
|
|
43
|
+
`steps` in order without contacting a provider:
|
|
44
|
+
|
|
45
|
+
- `clock` replaces the test clock for that and later steps.
|
|
46
|
+
- `subjectId` selects another subject for that step only. Otherwise use the
|
|
47
|
+
scenario's subject. This models trusted app composition, not a public request.
|
|
48
|
+
- `operation` names the semantic operation in the contract. Map it explicitly
|
|
49
|
+
to the framework's service in the test; do not expose dynamic method invocation
|
|
50
|
+
as an app endpoint.
|
|
51
|
+
- `input` is the operation input. For `reconcileEvent`, `facts` is the current
|
|
52
|
+
provider snapshot returned by the controlled loader after customer binding.
|
|
53
|
+
Raw webhook authentication is tested separately; these facts are not an
|
|
54
|
+
unsigned webhook payload to accept in production.
|
|
55
|
+
- `expect` asserts every listed field recursively. Arrays and primitive values
|
|
56
|
+
must match exactly. Unlisted result fields are permitted; extra array entries
|
|
57
|
+
are not. `error` instead requires that semantic error code and a rolled-back
|
|
58
|
+
operation, preserving state for later steps.
|
|
59
|
+
|
|
60
|
+
The sequence covers initial paid renewal, duplicate delivery, another delivery
|
|
61
|
+
for the same invoice, stable debit references, mismatched retry input, isolation
|
|
62
|
+
from another tenant, expiry exactly at period end, expired refunds, promotional
|
|
63
|
+
credits and rejection of overspending. Credit expiry and feature access are
|
|
64
|
+
separate assertions. An expired allowance never becomes a fresh grant on retry.
|
|
65
|
+
|
|
66
|
+
## Framework and CLI ownership
|
|
67
|
+
|
|
68
|
+
A Laravel application uses its own JSON parser/validator, policies, models,
|
|
69
|
+
transactions and test tools to implement these cases. An assistant can translate
|
|
70
|
+
the sequence into that app's PHPUnit/Pest tests without installing this JavaScript
|
|
71
|
+
runtime. The application selects its billable model and schema; the fixture does
|
|
72
|
+
not prescribe table names or copy JSKIT's storage internals into another framework.
|
|
73
|
+
|
|
74
|
+
A Node CLI consumer can run the same semantic calls against its installed JSKIT
|
|
75
|
+
services. Genesis may describe the project's test command, but these fixtures do
|
|
76
|
+
not require Genesis to execute it. Vibe64 configuration must preserve these same
|
|
77
|
+
values rather than inventing a different hosted payment format.
|
|
78
|
+
|
|
79
|
+
## Evidence boundaries
|
|
80
|
+
|
|
81
|
+
The package's `test/conformance.test.js` checks all configuration examples and
|
|
82
|
+
the full account sequence against the actual JSKIT service and SQLite storage.
|
|
83
|
+
An independent Python draft-07 validator plus the explicit reference rules also
|
|
84
|
+
checked the 11 configuration examples without importing Node or JSKIT. This
|
|
85
|
+
establishes independent interpretation of configuration, not a PHP payment engine.
|
|
86
|
+
|
|
87
|
+
Native Laravel execution, cross-process MySQL/PostgreSQL transactions, raw webhook
|
|
88
|
+
delivery and real provider checkout are separate evidence. These examples do not
|
|
89
|
+
certify them. The editor management-command conformance also remains a separate
|
|
90
|
+
consumer contract; portable account tests cannot substitute for its authorization,
|
|
91
|
+
review, source/release and environment checks.
|
package/docs/contract.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Portable payment contract v1
|
|
2
|
+
|
|
3
|
+
The application owns this contract and its runtime data. A UI and a CLI edit the
|
|
4
|
+
same `integrations.json` declaration at `extensions.payments`. Its normative
|
|
5
|
+
structural definition is `../contracts/configuration.schema.json`, shipped as
|
|
6
|
+
`@jskit-ai/payments-core/configuration.schema.json`. Any JSON Schema draft-07
|
|
7
|
+
validator can consume it. Cross-reference constraints below supplement it.
|
|
8
|
+
|
|
9
|
+
Example extension (inside the ordinary integrations document):
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"payments": {
|
|
14
|
+
"version": 1,
|
|
15
|
+
"environments": {
|
|
16
|
+
"sandbox": {
|
|
17
|
+
"integrationId": "billing",
|
|
18
|
+
"providerAccountId": "acct_replace_with_your_account",
|
|
19
|
+
"webhookSecretRef": "env:PAYMENT_WEBHOOK_SECRET",
|
|
20
|
+
"returnUrlRef": "env:PAYMENT_RETURN_URL"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"plans": {
|
|
24
|
+
"pro": {
|
|
25
|
+
"name": "Pro",
|
|
26
|
+
"amount": 1200,
|
|
27
|
+
"currency": "USD",
|
|
28
|
+
"interval": "month",
|
|
29
|
+
"features": ["export"],
|
|
30
|
+
"renewalCredits": 100
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Each integration reference must select a shared Stripe/Paddle API-key slot
|
|
38
|
+
using an Env reference. Paddle's connector environment must equal the payment
|
|
39
|
+
environment. Paddle also requires `taxCategory` and `publicClientTokenRef`
|
|
40
|
+
(an Env reference to its public frontend token). Each environment has its own merchant, signing secret and price
|
|
41
|
+
bindings. The same source declaration can name both environments, but callers
|
|
42
|
+
cannot select live operations merely by submitting `environment: live`.
|
|
43
|
+
The server owns environment selection and supplies the matching Env projection.
|
|
44
|
+
|
|
45
|
+
An amount is an integer in the currency's smallest unit. ISO-looking currency
|
|
46
|
+
syntax is not evidence the provider accepts that currency. Initial plans are
|
|
47
|
+
one recurring item, quantity one, monthly or yearly. Logical plan IDs are source
|
|
48
|
+
identities; external product and price IDs are environment/account-bound runtime
|
|
49
|
+
mappings. Publishing those mappings must preserve historical prices referenced
|
|
50
|
+
by existing subscriptions. Never infer a mapping from display names or assign
|
|
51
|
+
a browser-supplied provider price to a plan.
|
|
52
|
+
|
|
53
|
+
## Access and credits
|
|
54
|
+
|
|
55
|
+
A subscription grants its plan's named features only while its current fetched
|
|
56
|
+
status is `active` and its period end is in the future. Trials, past-due, unpaid,
|
|
57
|
+
paused, incomplete, expired and canceled subscriptions grant no features in v1.
|
|
58
|
+
Cancel-at-period-end remains active until the provider ends the subscription.
|
|
59
|
+
The app may deliberately add its own trial/free-feature policy outside this
|
|
60
|
+
paid entitlement check; it must not silently reinterpret these states.
|
|
61
|
+
|
|
62
|
+
An initial paid subscription period or ordinary paid renewal grants the plan's
|
|
63
|
+
`renewalCredits` once, keyed by the provider invoice/transaction, not delivery
|
|
64
|
+
ID. Zero means no grant. Proration/update events do not grant a fresh allowance.
|
|
65
|
+
Renewal credits expire at the paid period end and never roll over. Late delivery
|
|
66
|
+
may record already-expired credits; it must not give a new period accidentally.
|
|
67
|
+
Cancellation removes feature access but does not revoke an existing credit lot
|
|
68
|
+
before its recorded expiry. Monetary refunds/chargebacks do not yet implement
|
|
69
|
+
automatic credit clawback: that remains explicit application reconciliation.
|
|
70
|
+
|
|
71
|
+
A trusted app can grant top-up or promotional usage units with `grantCredits`:
|
|
72
|
+
positive integer `units`, stable business `reference`, and either an explicit
|
|
73
|
+
millisecond expiry or `null` for no expiry. This call does not collect money or
|
|
74
|
+
prove payment. Only call it after the app's own verified funding/business event.
|
|
75
|
+
|
|
76
|
+
Debits consume the soonest-expiring valid lot first and cannot create a negative
|
|
77
|
+
balance. A stable reference denotes one business action. Reusing it with a
|
|
78
|
+
different quantity fails. Full debit refunds happen once per debit, return
|
|
79
|
+
units only to still-valid lots, and report the amount already expired. Partial
|
|
80
|
+
refunds, monetary balances and transferable credits are outside v1 semantics.
|
|
81
|
+
|
|
82
|
+
Provider event receipts, subscription changes and allowance grants commit in
|
|
83
|
+
one transaction. Customer binding is unique within application, integration,
|
|
84
|
+
merchant and environment. Different delivery IDs for the same paid invoice
|
|
85
|
+
cannot produce another grant. Reconciliation loads current provider state;
|
|
86
|
+
provider event timestamps alone are insufficient ordering evidence.
|
|
87
|
+
|
|
88
|
+
## Framework implementation contract
|
|
89
|
+
|
|
90
|
+
Laravel uses its own authentication, policies, ORM/migrations, transaction and
|
|
91
|
+
row-locking APIs, provider PHP SDKs, routes and browser components. No PHP belongs
|
|
92
|
+
in this JavaScript package and no Node sidecar is required. A framework adapter
|
|
93
|
+
must preserve these observable behaviors:
|
|
94
|
+
|
|
95
|
+
| Operation | Required authority and outcome |
|
|
96
|
+
|---|---|
|
|
97
|
+
| Inspect account | Authenticated access to the billable subject; balance, features, subscriptions; no remote mutation |
|
|
98
|
+
| Billing history | Explicit billing-read policy for the subject; app-resolved customer, one filtered provider page, bounded display fields; no grant or mutation |
|
|
99
|
+
| Checkout | Billing-management policy, configured logical plan, trusted contact email and stable request ID; provider URL |
|
|
100
|
+
| Portal | Billing-management policy, server-bound customer; provider URL |
|
|
101
|
+
| Verify webhook | Raw bytes and signing secret for the exact environment; no reliance on a browser session |
|
|
102
|
+
| Reconcile event | Verified event, server customer binding, fresh provider facts and atomic receipt/state/grant |
|
|
103
|
+
| Debit/refund | Authorized app business action, stable reference and atomic balance change |
|
|
104
|
+
| Resolve uncertain operation | Explicit administrator policy and provider evidence; never blind retry |
|
|
105
|
+
|
|
106
|
+
Persistence must serialize mutations to the same scoped account and uniquely
|
|
107
|
+
index business receipts. A missing/failed transaction cannot fall back to
|
|
108
|
+
unlocked writes. Store customer mappings and catalogue bindings in the app's
|
|
109
|
+
database, not editor state. Retain an operation intent across crashes before
|
|
110
|
+
performing remote creation; refuse duplicates until reconciliation resolves it.
|
|
111
|
+
|
|
112
|
+
When integrating Laravel, give its assistant this document, the JSON schema and
|
|
113
|
+
[portable conformance fixtures](conformance.md),
|
|
114
|
+
the application's chosen database and billable identity, and its configured
|
|
115
|
+
provider. Ask it to use the installed framework/provider APIs to implement this
|
|
116
|
+
contract and test the fixture scenarios in the package tests. Those scenarios
|
|
117
|
+
are evidence requirements, not permission to import JavaScript into PHP or a
|
|
118
|
+
claim that a Laravel implementation already exists. The static fixtures require
|
|
119
|
+
no JavaScript execution. The application's editor-command implementation must
|
|
120
|
+
add the invoking host's operation contract separately; this package does not
|
|
121
|
+
parse Stack files or implement editor authorization.
|
|
122
|
+
|
|
123
|
+
Configuration failures use `payment_configuration_invalid` (422). Field errors
|
|
124
|
+
identify `extensions.payments/...` paths, including cross-reference failures at
|
|
125
|
+
`environments/<environment>/integrationId` and missing Paddle token/tax-category
|
|
126
|
+
fields. Messages explain the repair without echoing credential values.
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# Set up payments without an editor
|
|
2
|
+
|
|
3
|
+
A hand-written JSKIT application uses exactly the same payment declaration as an
|
|
4
|
+
application configured through an editor. The application reads the file; the
|
|
5
|
+
library receives the parsed configuration. There is no editor discovery, remote
|
|
6
|
+
configuration lookup, or dependency on an editor session.
|
|
7
|
+
|
|
8
|
+
Install `@jskit-ai/payments-core` and `@jskit-ai/connectors-core` alongside the
|
|
9
|
+
application's existing Knex integration and selected database driver. These
|
|
10
|
+
packages are currently worktree implementations: registry installation must wait
|
|
11
|
+
for the coordinated release. Run the payment package migrations through the
|
|
12
|
+
application's normal migration discovery before starting billing routes.
|
|
13
|
+
|
|
14
|
+
## Prepare provider access
|
|
15
|
+
|
|
16
|
+
Create an application-owned merchant account and key using the
|
|
17
|
+
[Stripe setup guide](../../connectors-catalog/docs/stripe.md) or
|
|
18
|
+
[Paddle setup guide](../../connectors-catalog/docs/paddle.md). The basic connector
|
|
19
|
+
check reads only balances or products; it does not verify billing permissions.
|
|
20
|
+
|
|
21
|
+
For the current Stripe payment adapter, the key must allow account and balance
|
|
22
|
+
reads, invoice and subscription reads, customer/product/price writes, and Checkout
|
|
23
|
+
and customer portal session creation. These correspond to the SDK calls in
|
|
24
|
+
`src/server/stripe.js`; grant only the resource permissions used by the app.
|
|
25
|
+
Stripe's [restricted-key guide](https://docs.stripe.com/keys/restricted-api-keys)
|
|
26
|
+
explains how to create/edit a key and inspect failed request logs. Connected-account
|
|
27
|
+
permissions are unnecessary for this app-owned merchant flow. Configure the
|
|
28
|
+
customer portal in Stripe's Billing settings separately.
|
|
29
|
+
|
|
30
|
+
For Paddle, enable `product.read`, `product.write`, `price.read`, `price.write`,
|
|
31
|
+
`customer.write`, `transaction.read`, `transaction.write`, `subscription.read`
|
|
32
|
+
and `customer_portal_session.write` for the current complete billing composition.
|
|
33
|
+
See the [permission reference](https://developer.paddle.com/api-reference/about/permissions/).
|
|
34
|
+
Notification destination editing additionally requires `notification_setting.write`;
|
|
35
|
+
domain approval lookup requires `checkout_domain.read`. Those connector operations
|
|
36
|
+
are separate from receiving and verifying webhooks.
|
|
37
|
+
|
|
38
|
+
For each environment, create a webhook destination pointing at the app's own
|
|
39
|
+
receiver and save its signing secret in the configured Env reference. Use the
|
|
40
|
+
[event selection instructions](../README.md#webhook-event-selection). A provider
|
|
41
|
+
API key is not a webhook signing secret. Keep sandbox and live credentials,
|
|
42
|
+
destinations and catalogue mappings separate. No live permission check or merchant
|
|
43
|
+
approval is inferred from the package's controlled tests.
|
|
44
|
+
|
|
45
|
+
## Declare the application's plans
|
|
46
|
+
|
|
47
|
+
Add this payment extension to the application's integration document. `billing`
|
|
48
|
+
refers to a shared Stripe integration whose API key is an Env reference. The
|
|
49
|
+
merchant account ID below is illustrative; replace it with the actual account.
|
|
50
|
+
Keep the complete integration document valid under the connector schema too.
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"version": 1,
|
|
55
|
+
"environments": {
|
|
56
|
+
"sandbox": {
|
|
57
|
+
"integrationId": "billing",
|
|
58
|
+
"providerAccountId": "acct_replace_me",
|
|
59
|
+
"webhookSecretRef": "env:STRIPE_WEBHOOK_SECRET",
|
|
60
|
+
"returnUrlRef": "env:BILLING_RETURN_URL"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"plans": {
|
|
64
|
+
"pro": {
|
|
65
|
+
"name": "Pro",
|
|
66
|
+
"amount": 1200,
|
|
67
|
+
"currency": "USD",
|
|
68
|
+
"interval": "month",
|
|
69
|
+
"features": ["export"],
|
|
70
|
+
"renewalCredits": 100
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Save that object at `extensions.payments` in `integrations.json`. Keep the
|
|
77
|
+
provider API key in the environment named by
|
|
78
|
+
`integrations.billing.authentication.secretRef`. Set the signing secret and
|
|
79
|
+
return URL through the application's normal environment mechanism. Missing
|
|
80
|
+
values, including the placeholder `MISSING`, must fail setup visibly.
|
|
81
|
+
|
|
82
|
+
## Compose the server libraries
|
|
83
|
+
|
|
84
|
+
This is application startup code. `knex`, `authorizeBilling`, `applicationId`,
|
|
85
|
+
`environment` and `configurationUrl` are supplied by the application's existing
|
|
86
|
+
server composition. Select them on the server. `applicationId` is a durable
|
|
87
|
+
application identity, not its current domain. The environment is explicitly
|
|
88
|
+
`sandbox` or `live`; do not silently fall back between them.
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
import { readFile } from 'node:fs/promises';
|
|
92
|
+
import { createEnvironmentReferenceResolver } from '@jskit-ai/connectors-core/server';
|
|
93
|
+
import { validatePaymentConfiguration } from '@jskit-ai/payments-core/shared';
|
|
94
|
+
import { createKnexPaymentStore } from '@jskit-ai/payments-core/server/storage';
|
|
95
|
+
import { createPaymentService } from '@jskit-ai/payments-core/server';
|
|
96
|
+
import { createStripePaymentAdapter } from '@jskit-ai/payments-core/server/stripe';
|
|
97
|
+
import { createPaymentCheckoutService } from '@jskit-ai/payments-core/server/checkout';
|
|
98
|
+
import { createPaymentCatalogue } from '@jskit-ai/payments-core/server/catalogue';
|
|
99
|
+
|
|
100
|
+
const document = JSON.parse(await readFile(configurationUrl, 'utf8'));
|
|
101
|
+
// Also run the application's complete connector validation with its provider list.
|
|
102
|
+
const configuration = validatePaymentConfiguration(document);
|
|
103
|
+
if (!Object.hasOwn(configuration.environments, environment)) {
|
|
104
|
+
throw new Error('Configure the selected payment environment first.');
|
|
105
|
+
}
|
|
106
|
+
const binding = configuration.environments[environment];
|
|
107
|
+
const integration = document.integrations[binding.integrationId];
|
|
108
|
+
if (integration.provider !== 'stripe') throw new Error('This composition selects Stripe.');
|
|
109
|
+
const resolve = createEnvironmentReferenceResolver(process.env);
|
|
110
|
+
const apiKey = await resolve(integration.authentication.secretRef);
|
|
111
|
+
const webhookSecret = await resolve(binding.webhookSecretRef);
|
|
112
|
+
const returnUrl = await resolve(binding.returnUrlRef);
|
|
113
|
+
const merchantScope = {
|
|
114
|
+
applicationId, integrationId: binding.integrationId,
|
|
115
|
+
providerAccountId: binding.providerAccountId, environment
|
|
116
|
+
};
|
|
117
|
+
const store = createKnexPaymentStore({ knex });
|
|
118
|
+
const payments = createPaymentService({ store, configuration });
|
|
119
|
+
const published = await store.inspectCatalogue(merchantScope);
|
|
120
|
+
const priceBindings = Object.fromEntries(Object.entries(published.plans)
|
|
121
|
+
.filter(([planId, plan]) => Object.hasOwn(configuration.plans, planId) && plan.priceId)
|
|
122
|
+
.map(([planId, plan]) => [planId, plan.priceId]));
|
|
123
|
+
const historicalPriceBindings = Object.fromEntries(published.history
|
|
124
|
+
.map((plan) => [plan.priceId, plan.planId]));
|
|
125
|
+
const adapter = createStripePaymentAdapter({
|
|
126
|
+
apiKey, webhookSecret, environment, providerAccountId: binding.providerAccountId,
|
|
127
|
+
priceBindings, historicalPriceBindings
|
|
128
|
+
});
|
|
129
|
+
const catalogue = createPaymentCatalogue({ store, adapter, scope: merchantScope, configuration });
|
|
130
|
+
const checkout = createPaymentCheckoutService({
|
|
131
|
+
adapter, store, payments, merchantScope, returnUrl, authorize: authorizeBilling
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The returned objects stay inside the backend. Never serialize the environment,
|
|
136
|
+
adapter or secret-bearing startup variables to the client. `authorizeBilling`
|
|
137
|
+
must return exactly `true` only for an actor allowed to manage the requested
|
|
138
|
+
billable subject and action. A subject can be a workspace or user; the app owns
|
|
139
|
+
that decision and obtains its ID from authenticated membership, not request
|
|
140
|
+
claims alone.
|
|
141
|
+
|
|
142
|
+
An authorized administrator first calls `catalogue.preview()`, reviews its
|
|
143
|
+
changes and calls `catalogue.publish({reviewId})`. An ordinary application CLI
|
|
144
|
+
can do this. Configuration reads never publish anything. After successful
|
|
145
|
+
publication, rebuild this composition from the current bindings (or restart the
|
|
146
|
+
app) before accepting checkout requests. An empty catalogue cannot sell a plan.
|
|
147
|
+
|
|
148
|
+
Map authenticated checkout and portal routes to the corresponding `checkout`
|
|
149
|
+
methods. Map the webhook route to `checkout.webhook({rawBody, signature})`,
|
|
150
|
+
preserving the unmodified request Buffer. Enforce features using
|
|
151
|
+
`payments.requireFeature({...merchantScope, subjectId}, 'export')`; debit credits
|
|
152
|
+
using a stable business-operation reference. A successful checkout redirect is
|
|
153
|
+
not proof of payment: verified provider reconciliation updates access and credits.
|
|
154
|
+
|
|
155
|
+
## Paddle composition
|
|
156
|
+
|
|
157
|
+
Use `createPaddlePaymentAdapter` from `@jskit-ai/payments-core/server/paddle`
|
|
158
|
+
with the same store, services and mappings, plus the binding's explicit
|
|
159
|
+
`taxCategory`. The selected Paddle integration's environment must match the
|
|
160
|
+
payment environment. Resolve `publicClientTokenRef` separately and supply only
|
|
161
|
+
that public client token to the app's Paddle.js checkout page. Never supply its
|
|
162
|
+
API key or signing secret to the browser. The return URL must be the approved
|
|
163
|
+
application checkout page described in the package README. Merchant approval
|
|
164
|
+
and checkout-domain approval remain provider setup steps.
|
|
165
|
+
|
|
166
|
+
## Changes and portability
|
|
167
|
+
|
|
168
|
+
Editing the JSON file manually changes the same configuration the editor would
|
|
169
|
+
change. Restart/reload the application composition deliberately after validation;
|
|
170
|
+
the library does not watch files. Price changes need a reviewed catalogue publish.
|
|
171
|
+
Keep old plans available until existing subscribers have an explicit migration
|
|
172
|
+
policy; removing a plan does not cancel subscriptions or invent replacement access.
|
|
173
|
+
|
|
174
|
+
Move the source, environment bindings and application database together when
|
|
175
|
+
changing hosts. Back up customer mappings, publication mappings and credit
|
|
176
|
+
entries with the rest of the database. Domain changes require the app's return
|
|
177
|
+
URL and provider webhook/checkout settings to be updated; they do not change
|
|
178
|
+
its application identity or move ownership to an editor.
|
|
179
|
+
|
|
180
|
+
This guide describes explicit composition, not a completed automatic installer.
|
|
181
|
+
HTTP framework wiring and native PostgreSQL/MySQL deployment verification remain
|
|
182
|
+
application responsibilities; focused package tests use controlled fixtures.
|
|
183
|
+
|
|
184
|
+
## Customer-facing billing page
|
|
185
|
+
|
|
186
|
+
The optional `@jskit-ai/payments-web` package owns the reusable Vue/Vuetify
|
|
187
|
+
`PaymentAccount` view. Its README specifies the props, emitted actions and
|
|
188
|
+
existing JSKIT HTTP-hook composition. Node-only consumers do not install Vue.
|
|
189
|
+
`checkout.account({actor, subjectId})` authorizes action `account`, then returns
|
|
190
|
+
balance, features, subscriptions and a `hasCustomer` flag without provider customer
|
|
191
|
+
IDs or credentials. The application adds safe display plans and permission hints.
|
|
192
|
+
Checkout and portal calls still authorize their own actions independently.
|
|
193
|
+
|
|
194
|
+
## Provider billing history from an app or CLI
|
|
195
|
+
|
|
196
|
+
The same checkout service exposes `history({actor, subjectId, collection, after})`.
|
|
197
|
+
Authorize action `history` using the application's billing-read policy. An editor
|
|
198
|
+
owner is not automatically authorized to read a tenant's records. The service
|
|
199
|
+
resolves the provider customer from the scoped application database; callers
|
|
200
|
+
cannot select a provider customer, merchant or environment.
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
// actor and subjectId come from the app's authenticated route or CLI policy.
|
|
204
|
+
const page = await checkout.history({
|
|
205
|
+
actor, subjectId, collection: 'transactions', after: null
|
|
206
|
+
});
|
|
207
|
+
// Display this page; load another only on an explicit request, using nextCursor.
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Collections are `subscriptions` and `transactions`. Each call fetches at most 20
|
|
211
|
+
records and returns `{collection, items, nextCursor}`. A missing customer returns
|
|
212
|
+
an empty page without creating one. Items expose only provider ID, kind, status,
|
|
213
|
+
and ISO creation time. Financial records also expose currency, `totalMinor` and
|
|
214
|
+
`paidMinor` as integer strings or `null` when unknown; preserve these strings
|
|
215
|
+
when using native decimal/money facilities. Do not convert large amounts to a
|
|
216
|
+
JavaScript Number.
|
|
217
|
+
|
|
218
|
+
Stripe financial history contains **invoices**, not every charge, refund or
|
|
219
|
+
payment attempt. Paddle contains **transactions**; its total is not a paid
|
|
220
|
+
amount, so `paidMinor` stays null. Draft Paddle totals may also be null. Preserve
|
|
221
|
+
the `kind` and status in the UI rather than labelling every row a successful
|
|
222
|
+
payment. Subscription history includes canceled subscriptions and is a provider
|
|
223
|
+
view, not the source of application entitlements. A read never grants credits.
|
|
224
|
+
|
|
225
|
+
Laravel can use its native policies and stored customer relation to call
|
|
226
|
+
[Stripe subscription lists](https://docs.stripe.com/api/subscriptions/list) and
|
|
227
|
+
[invoice lists](https://docs.stripe.com/api/invoices/list), or
|
|
228
|
+
[Paddle subscription lists](https://developer.paddle.com/api-reference/subscriptions/list-subscriptions/)
|
|
229
|
+
and [transaction lists](https://developer.paddle.com/api-reference/transactions/list-transactions/).
|
|
230
|
+
Apply the customer filter on every page, retain provider status, and project only
|
|
231
|
+
these display fields. Do not pass arbitrary pagination URLs or SDK objects to
|
|
232
|
+
the client. JSKIT's provider adapters are trusted server primitives; UI/CLI
|
|
233
|
+
entry points use the authorized service above.
|
|
234
|
+
|
|
235
|
+
This is an app/CLI library capability. The editor history command and history
|
|
236
|
+
screens remain separate integration work; this method does not add them by itself.
|
|
237
|
+
|
|
238
|
+
## Read-only readiness inspection
|
|
239
|
+
|
|
240
|
+
Compose `createPaymentReadiness` from
|
|
241
|
+
`@jskit-ai/payments-core/server/readiness` with `{adapter, catalogue,
|
|
242
|
+
scope: merchantScope}`. Its `inspect()` reports separate credential, account,
|
|
243
|
+
charges, payouts, catalogue, webhook, checkout, site and deployment checks. No
|
|
244
|
+
provider writes occur. Scope mismatches fail, provider exceptions are replaced
|
|
245
|
+
with safe diagnostics, and unknown/manual checks remain explicit.
|
|
246
|
+
|
|
247
|
+
An optional app-owned `inspectApplication` callback may supply `webhook`,
|
|
248
|
+
`checkout`, `site` and `deployment` evidence as `{status, detail}` records. Use
|
|
249
|
+
`passed`, `failed`, `unknown` or `manual` and a safe explanation of at most 500
|
|
250
|
+
characters. Record only observed checks. Never turn an unavailable check into
|
|
251
|
+
success or claim provider approval from a fixture. This callback is a normal app
|
|
252
|
+
function, not a Vibe64 API. Authorize any route/CLI wrapper exposing the service.
|
|
253
|
+
|
|
254
|
+
Catalogue recovery requires a fresh `catalogue.preview()` and its `reviewId`:
|
|
255
|
+
`catalogue.recover({reviewId, providerId})`. The adapter reads the candidate object;
|
|
256
|
+
its ID and pending operation must match, and a recovered price must be active.
|
|
257
|
+
A stale review is rejected. Trusted administrator code may instead supply
|
|
258
|
+
`confirmedNotCreated: true` with that review only after actual provider inspection
|
|
259
|
+
establishes absence. Do not expose that assertion as an ordinary browser flag.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
exports.up = async function up(knex) {
|
|
2
|
+
await knex.schema.createTable("payment_catalogues", (table) => {
|
|
3
|
+
table.string("catalogue_key", 64).primary();
|
|
4
|
+
table.text("payload", "mediumtext").notNullable();
|
|
5
|
+
});
|
|
6
|
+
await knex.schema.createTable("payment_accounts", (table) => {
|
|
7
|
+
table.string("account_key", 64).primary();
|
|
8
|
+
table.text("payload", "mediumtext").notNullable();
|
|
9
|
+
});
|
|
10
|
+
await knex.schema.createTable("payment_entries", (table) => {
|
|
11
|
+
table.string("entry_key", 64).primary();
|
|
12
|
+
table.string("account_key", 64).notNullable().references("account_key").inTable("payment_accounts");
|
|
13
|
+
table.text("payload", "mediumtext").notNullable();
|
|
14
|
+
});
|
|
15
|
+
await knex.schema.createTable("payment_customers", (table) => {
|
|
16
|
+
table.string("customer_key", 64).primary();
|
|
17
|
+
table.string("subject_id", 200).notNullable();
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
exports.down = async function down(knex) {
|
|
22
|
+
await knex.schema.dropTable("payment_customers");
|
|
23
|
+
await knex.schema.dropTable("payment_entries");
|
|
24
|
+
await knex.schema.dropTable("payment_accounts");
|
|
25
|
+
await knex.schema.dropTable("payment_catalogues");
|
|
26
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jskit-ai/payments-core",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Application-owned payment configuration, subscription access and transactional usage credits.",
|
|
6
|
+
"exports": {
|
|
7
|
+
"./shared": "./src/shared/configuration.js",
|
|
8
|
+
"./configuration.schema.json": "./contracts/configuration.schema.json",
|
|
9
|
+
"./conformance.json": "./contracts/conformance.json",
|
|
10
|
+
"./docs/contract.md": "./docs/contract.md",
|
|
11
|
+
"./docs/conformance.md": "./docs/conformance.md",
|
|
12
|
+
"./server": "./src/server/service.js",
|
|
13
|
+
"./server/storage": "./src/server/knexStore.js",
|
|
14
|
+
"./server/checkout": "./src/server/checkout.js",
|
|
15
|
+
"./server/stripe": "./src/server/stripe.js",
|
|
16
|
+
"./server/paddle": "./src/server/paddle.js",
|
|
17
|
+
"./server/catalogue": "./src/server/catalogue.js",
|
|
18
|
+
"./server/readiness": "./src/server/readiness.js"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node --test --test-concurrency=1"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@paddle/paddle-node-sdk": "3.10.0",
|
|
25
|
+
"ajv": "8.20.0",
|
|
26
|
+
"stripe": "22.6.2"
|
|
27
|
+
},
|
|
28
|
+
"jskit": {
|
|
29
|
+
"kind": "runtime",
|
|
30
|
+
"migrations": {
|
|
31
|
+
"directories": [
|
|
32
|
+
"migrations"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"metadata": {
|
|
36
|
+
"jskit": {
|
|
37
|
+
"tableOwnership": {
|
|
38
|
+
"tables": [
|
|
39
|
+
{
|
|
40
|
+
"tableName": "payment_accounts"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"tableName": "payment_entries"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"tableName": "payment_customers"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"tableName": "payment_catalogues"
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"capabilities": {
|
|
56
|
+
"provides": [],
|
|
57
|
+
"requires": []
|
|
58
|
+
},
|
|
59
|
+
"runtime": {
|
|
60
|
+
"server": {
|
|
61
|
+
"providers": []
|
|
62
|
+
},
|
|
63
|
+
"client": {
|
|
64
|
+
"providers": []
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|