kempo-payments 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 +400 -0
- package/admin/components/PaymentDetail.js +304 -0
- package/admin/components/PaymentList.js +261 -0
- package/admin/components/ProviderSettings.js +310 -0
- package/admin/components/TestPayment.js +283 -0
- package/admin/index.page.html +81 -0
- package/admin/nav.global.html +3 -0
- package/install.js +48 -0
- package/kempo-config.json +73 -0
- package/package.json +56 -0
- package/public/api/config/GET.js +48 -0
- package/public/api/payments/GET.js +31 -0
- package/public/api/payments/POST.js +41 -0
- package/public/api/payments/[id]/GET.js +30 -0
- package/public/api/payments/[id]/cancel/POST.js +19 -0
- package/public/api/payments/[id]/capture/POST.js +22 -0
- package/public/api/payments/[id]/client-secret/GET.js +23 -0
- package/public/api/payments/[id]/refund/POST.js +28 -0
- package/public/api/payments/[id]/sync/POST.js +23 -0
- package/public/api/providers/GET.js +16 -0
- package/public/api/settings/GET.js +36 -0
- package/public/api/settings/PUT.js +123 -0
- package/public/api/summary/GET.js +18 -0
- package/public/api/webhooks/[provider]/POST.js +37 -0
- package/public/components/PaymentForm.js +522 -0
- package/public/sdk.js +72 -0
- package/public/utils/money.js +67 -0
- package/sdk.js +42 -0
- package/server/db/schema.js +141 -0
- package/server/providers/index.js +84 -0
- package/server/providers/stripe.js +435 -0
- package/server/utils/config/declared.js +25 -0
- package/server/utils/config/settings.js +72 -0
- package/server/utils/money/currencies.js +67 -0
- package/server/utils/payments/actions.js +149 -0
- package/server/utils/payments/create.js +116 -0
- package/server/utils/payments/statuses.js +55 -0
- package/server/utils/payments/store.js +250 -0
- package/server/utils/payments/sync.js +179 -0
- package/server/utils/payments/text.js +38 -0
- package/server/utils/permissions/gate.js +42 -0
- package/server/utils/webhooks/receive.js +153 -0
- package/uninstall.js +35 -0
- package/update.js +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# kempo-payments
|
|
2
|
+
|
|
3
|
+
A payment-processing extension for the [kempo](https://github.com/dustinpoissant/kempo) CMS. Take a
|
|
4
|
+
payment, capture an authorisation, issue a refund — without the code doing it knowing which processor
|
|
5
|
+
is behind it.
|
|
6
|
+
|
|
7
|
+
Stripe is the processor that ships. It is not the one the rest of the extension is written against:
|
|
8
|
+
everything goes through a small provider interface, and adding PayPal or Square later is a new file
|
|
9
|
+
in `server/providers/`, not a change to anything that ever called `createPayment`.
|
|
10
|
+
|
|
11
|
+
This kempo (CMS) extension depends on no other kempo extension. Taking money does not require a
|
|
12
|
+
product catalog, so there is no dependency on `kempo-commerce` — the arrow points the other way. That is also what makes it usable
|
|
13
|
+
for anything else that needs to charge a card: a subscription, a donation, a one-off invoice.
|
|
14
|
+
|
|
15
|
+
## What it is for
|
|
16
|
+
|
|
17
|
+
Other kempo (CMS) extensions. `kempo-payments` has no checkout of its own and no idea what anybody is buying.
|
|
18
|
+
It knows how to move money and how to tell you it moved.
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import { createPayment } from 'kempo-payments/sdk';
|
|
22
|
+
|
|
23
|
+
const [error, { payment, clientSecret }] = await createPayment({
|
|
24
|
+
amount: 4999, // minor units, always — 4999 is $49.99
|
|
25
|
+
currency: 'usd',
|
|
26
|
+
owner: 'kempo-commerce', // who is asking
|
|
27
|
+
reference: order.id, // their own id for what is being paid for
|
|
28
|
+
userId: user?.id, // null for a guest
|
|
29
|
+
customerEmail: order.email,
|
|
30
|
+
description: `Order ${order.number}`,
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Hand `clientSecret` to the browser, mount the payment form against it, and then **stop watching the
|
|
35
|
+
browser**. Fulfilment hangs off the `payment:succeeded` hook, which is fired from the processor's
|
|
36
|
+
webhook — see [Hooks](#hooks-it-fires) for why that distinction is the whole design.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install kempo-payments
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Then install it from the admin panel, or:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
import installExtension from 'kempo/server/utils/extensions/installExtension.js';
|
|
48
|
+
await installExtension({ name: 'kempo-payments' });
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Requires kempo **4.2.40 or later** — the first release with the encrypted `secret` setting type that
|
|
52
|
+
every credential here is stored as.
|
|
53
|
+
|
|
54
|
+
### `SETTINGS_ENCRYPTION_KEY` is required
|
|
55
|
+
|
|
56
|
+
Every credential this extension stores is a kempo `secret` setting, encrypted at rest. kempo cannot
|
|
57
|
+
encrypt anything without a key, so without one **no credential can be saved at all** — the settings
|
|
58
|
+
screen refuses, and installing prints a warning rather than leaving you to find out later.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Put it in the site's environment as `SETTINGS_ENCRYPTION_KEY` and restart. Back it up in the same
|
|
65
|
+
place as `DATABASE_URL`: losing it makes every stored secret on the site unrecoverable, and rotating
|
|
66
|
+
it means re-entering every one of them.
|
|
67
|
+
|
|
68
|
+
## Connecting Stripe
|
|
69
|
+
|
|
70
|
+
Everything below is on the extension's own admin screen at
|
|
71
|
+
`/admin/extension/kempo-payments/` → **Settings**.
|
|
72
|
+
|
|
73
|
+
1. **Get the keys.** Stripe dashboard → Developers → API keys. In test mode these begin `pk_test_`
|
|
74
|
+
and `sk_test_`. The secret key is shown once.
|
|
75
|
+
2. **Paste both in and save.** The screen says which mode the keys are for, read off the key
|
|
76
|
+
prefixes rather than a switch — a switch can disagree with the keys, and "the toggle said test"
|
|
77
|
+
is an expensive sentence.
|
|
78
|
+
3. **Add the webhook endpoint.** The screen shows the URL and the exact list of events to subscribe
|
|
79
|
+
it to. Stripe gives you a signing secret (`whsec_…`) when you create it; paste that in too.
|
|
80
|
+
4. **Take a test payment.** The **Take a payment** tab runs the whole path — an intent created with
|
|
81
|
+
the secret key, the real payment form built with the publishable key, a confirmation from the
|
|
82
|
+
browser, and then the webhook coming back. Card `4242 4242 4242 4242`, any future expiry, any
|
|
83
|
+
CVC.
|
|
84
|
+
|
|
85
|
+
If step 4 ends at "Paid, but nothing came back", the charge worked and the webhook did not. That is
|
|
86
|
+
the failure worth catching before launch, because in production it means orders that are paid for
|
|
87
|
+
and never fulfilled.
|
|
88
|
+
|
|
89
|
+
### Webhooks on localhost
|
|
90
|
+
|
|
91
|
+
Stripe cannot reach a development machine, so forward events to it instead:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
stripe login
|
|
95
|
+
stripe listen --forward-to localhost:9876/payments/api/webhooks/stripe
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`stripe listen` prints its **own** signing secret, different from the dashboard endpoint's. That is
|
|
99
|
+
the one to paste into the settings screen while developing.
|
|
100
|
+
|
|
101
|
+
## Wallets and buy-now-pay-later
|
|
102
|
+
|
|
103
|
+
Apple Pay, Google Pay, Klarna, Afterpay, Cash App Pay and Amazon Pay are not separate providers
|
|
104
|
+
here and never will be. `<k-pay-form>`'s Express Checkout button row offers whichever of them are
|
|
105
|
+
available to a given customer, drawn and driven entirely by Stripe — no extra integration, no
|
|
106
|
+
separate merchant account, and no code in this extension to add one. Modelling any of them as its
|
|
107
|
+
own provider module would duplicate an integration that is already done.
|
|
108
|
+
|
|
109
|
+
"Available" is a dashboard setting, not a code path: Apple Pay and Google Pay need the site's domain
|
|
110
|
+
registered with Stripe (Settings → Payment methods → Apple Pay, and HTTPS — neither works over
|
|
111
|
+
plain HTTP, which is why they are absent in local development); Klarna, Cash App Pay and Amazon Pay
|
|
112
|
+
need to be switched on the same way. Stripe shows the not-yet-activated ones anyway in test mode, as
|
|
113
|
+
a preview of what turning them on would look like — genuinely useful for building a checkout, worth
|
|
114
|
+
knowing it means nothing about what a real customer will see until the dashboard step is done.
|
|
115
|
+
|
|
116
|
+
Two of them are not iframes at all. Tapping the Apple Pay or Google Pay button opens the *native OS
|
|
117
|
+
payment sheet* — Face ID, the device's own card picker — not anything this extension or Stripe
|
|
118
|
+
draws; the wallet encrypts the card on-device and only ever hands Stripe an already-encrypted
|
|
119
|
+
token. Klarna, Afterpay and similar hand the customer to a screen that has to stay the provider's:
|
|
120
|
+
they are running a soft credit check or an underwriting decision that only they can make. There is
|
|
121
|
+
no "backend only" version of that step, which is also why this extension has no custom UI of its
|
|
122
|
+
own for any of them — Stripe's Express Checkout button, tapped, is the entire integration.
|
|
123
|
+
|
|
124
|
+
## Roadmap processors
|
|
125
|
+
|
|
126
|
+
Real second and third integrations, each with its own SDK, settlement and dispute model. Not
|
|
127
|
+
sequenced yet:
|
|
128
|
+
|
|
129
|
+
- **PayPal** — its Checkout SDK now covers what Braintree used to, which is why Braintree is not on
|
|
130
|
+
this list rather than an oversight.
|
|
131
|
+
- **Square** — the obvious pairing for a site with a physical till.
|
|
132
|
+
- **Mollie** — European coverage Stripe prices differently.
|
|
133
|
+
- **Amazon Pay**.
|
|
134
|
+
|
|
135
|
+
**Adyen** was considered and excluded: its onboarding is enterprise, application-based and
|
|
136
|
+
account-manager-mediated, which does not fit a self-serve platform a small seller installs.
|
|
137
|
+
|
|
138
|
+
## The payment form
|
|
139
|
+
|
|
140
|
+
A reusable custom element, served from this extension:
|
|
141
|
+
|
|
142
|
+
```html
|
|
143
|
+
<script type="module" src="/payments/components/PaymentForm.js"></script>
|
|
144
|
+
|
|
145
|
+
<k-pay-form
|
|
146
|
+
client-secret="pi_3..._secret_..."
|
|
147
|
+
return-url="https://example.com/thank-you"
|
|
148
|
+
submit-label="Pay $49.99"
|
|
149
|
+
></k-pay-form>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
It shows two things, either of which may be absent depending on what is available:
|
|
153
|
+
|
|
154
|
+
- **Express Checkout** — a row of wallet buttons (Apple Pay, Google Pay, Link, and PayPal or Amazon
|
|
155
|
+
Pay if enabled), drawn and managed entirely by Stripe. Automatically hidden when nothing is
|
|
156
|
+
available for that customer's browser/account, with no gap left behind.
|
|
157
|
+
- **Card fields** — number, expiry, CVC and a plain ZIP/postal input, laid out and labelled by this
|
|
158
|
+
extension's own markup. Each of the three card fields is a small iframe belonging to the
|
|
159
|
+
processor; typing sends the digits from the customer's browser straight to it, never through this
|
|
160
|
+
server, which is what keeps a kempo site out of PCI scope. There is no supported way to collect a
|
|
161
|
+
card number in a plain kempo-ui `<input>` without taking on PCI scope yourself; this is as close
|
|
162
|
+
as a card field gets to looking like the rest of the form.
|
|
163
|
+
|
|
164
|
+
Neither half of the modern `appearance` API reaches these three fields — confirmed by testing,
|
|
165
|
+
twice over, not assumed. Their box — background, border, radius, padding, focus ring — is real CSS
|
|
166
|
+
on the wrapper `<div>` (a `.k-pay-field` class this component injects once, copied property-for-
|
|
167
|
+
property from kempo-css's own `input` rule), because these particular Stripe fields render with a
|
|
168
|
+
transparent, unpadded iframe by design — the caller's page has always been expected to draw the
|
|
169
|
+
box — and `appearance.rules` (which does restyle Stripe's *newer* elements, Express Checkout
|
|
170
|
+
included) does not reach them at all. Their text colour uses the `style` option instead —
|
|
171
|
+
`style.base.color` / `style.base['::placeholder']`, Stripe's original, pre-`appearance` styling
|
|
172
|
+
mechanism for exactly this element family — because `appearance.variables.colorText` doesn't reach
|
|
173
|
+
them either, the same silent no-effect failure one layer deeper.
|
|
174
|
+
|
|
175
|
+
> **Do not put `<k-pay-form>` inside a shadow root.** Stripe's fields do not mount in one — they
|
|
176
|
+
> reserve their height and render nothing, with no error anywhere. If a checkout shows an empty box
|
|
177
|
+
> where a card field should be, this is why: something in the chain of custom elements above it is
|
|
178
|
+
> using shadow DOM. Everything hosting the form has to be light DOM.
|
|
179
|
+
|
|
180
|
+
Klarna, Afterpay, Cash App Pay and Amazon Pay are not excluded — see
|
|
181
|
+
[Wallets and buy-now-pay-later](#wallets-and-buy-now-pay-later) for how they appear through Express
|
|
182
|
+
Checkout the same as Apple Pay and Google Pay do, gated by dashboard activation rather than by
|
|
183
|
+
anything in this component. What this rewrite did trade away is Stripe's unified, all-methods,
|
|
184
|
+
single-tabbed-widget layout (the previous version of this component) in exchange for owning the
|
|
185
|
+
card fields' own look. Wanting that back is one component, not a rewrite: mount
|
|
186
|
+
`elements.create('payment')` instead of the split card fields and Express Checkout Element — the
|
|
187
|
+
server side of this extension does not care which client-side widget produced the confirmation.
|
|
188
|
+
|
|
189
|
+
| Event | When |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `payment-succeeded` | the processor confirmed it in this browser |
|
|
192
|
+
| `payment-processing` | accepted, still settling — normal for bank debits |
|
|
193
|
+
| `payment-failed` | declined, or the details were rejected |
|
|
194
|
+
|
|
195
|
+
Use these to move the customer along. **Do not use them to fulfil an order.** A browser closed at
|
|
196
|
+
the wrong moment fires nothing at all, and a browser under someone's control fires whatever they
|
|
197
|
+
like.
|
|
198
|
+
|
|
199
|
+
## Hooks it fires
|
|
200
|
+
|
|
201
|
+
Registered by consumers in their own `kempo-config.json`, the same as any other kempo hook:
|
|
202
|
+
|
|
203
|
+
```json
|
|
204
|
+
"hooks": {
|
|
205
|
+
"payment:succeeded": "hooks/payment-succeeded.js"
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
| Event | Data | When |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| `payment:created` | `{ payment }` | an attempt has been started; no money has moved |
|
|
212
|
+
| `payment:authorized` | `{ payment }` | held on the customer's card, waiting to be captured |
|
|
213
|
+
| `payment:succeeded` | `{ payment }` | **taken** — this is the one to fulfil against |
|
|
214
|
+
| `payment:failed` | `{ payment, error }` | the processor refused it |
|
|
215
|
+
| `payment:canceled` | `{ payment }` | abandoned, or the hold released |
|
|
216
|
+
| `payment:refunded` | `{ payment, refund }` | some or all of it given back |
|
|
217
|
+
| `payment:disputed` | `{ payment, disputed, reason }` | a chargeback opened, or closed |
|
|
218
|
+
|
|
219
|
+
`payment:succeeded` fires **once** per payment, on the transition into that state and nowhere else.
|
|
220
|
+
Redelivered webhooks, polling and admin screens all pass through the same place and produce no
|
|
221
|
+
transition, so nothing downstream sends a second confirmation email or ships a second box.
|
|
222
|
+
|
|
223
|
+
## Capture: automatic or manual
|
|
224
|
+
|
|
225
|
+
**Automatic** charges the card the moment the customer confirms. Right for anything delivered
|
|
226
|
+
immediately.
|
|
227
|
+
|
|
228
|
+
**Manual** places a hold and takes nothing until somebody captures it. Right for anything that
|
|
229
|
+
ships, where the amount can change or the order can be cancelled before it goes out. The hold
|
|
230
|
+
expires on its own — typically after seven days — and an authorisation nobody captured is money the
|
|
231
|
+
customer had reserved and the merchant never received.
|
|
232
|
+
|
|
233
|
+
The site-wide default is a setting; any individual payment can override it.
|
|
234
|
+
|
|
235
|
+
A capture method is **never guessed at**. `Manual` and `MANUAL ` are read as `manual` — case and
|
|
236
|
+
stray spaces are forgiven — but anything else (`hold`, `authorize`, a typo) is refused with a 400
|
|
237
|
+
naming the two that exist, before any processor is contacted. The alternative was falling back to
|
|
238
|
+
`automatic`, which turns "reserve this card" into "charge this card" for every mistyped word. The
|
|
239
|
+
same rule applies to the setting on the settings screen.
|
|
240
|
+
|
|
241
|
+
## Refunds and cancellations
|
|
242
|
+
|
|
243
|
+
They are not the same thing and the extension will not let them be confused:
|
|
244
|
+
|
|
245
|
+
- A payment that is **authorised but not captured** has had nothing taken. Releasing it is a
|
|
246
|
+
**cancel**. Asking to refund it is refused, with that sentence rather than the processor's.
|
|
247
|
+
- A payment that **succeeded** can be refunded, in full or in part, repeatedly, until nothing is
|
|
248
|
+
left outstanding.
|
|
249
|
+
|
|
250
|
+
The refunded total is recomputed from the individual refund rows rather than incremented, and every
|
|
251
|
+
refund is keyed by the processor's own reference. An admin refund and the webhook reporting the same
|
|
252
|
+
refund a second later are one row and one total — which is what stops a payment claiming twice the
|
|
253
|
+
money went back.
|
|
254
|
+
|
|
255
|
+
Refunds made directly in the processor's dashboard arrive by webhook and are recorded here too.
|
|
256
|
+
Stripe's `charge.refunded` event reports only the new running total, not which refunds make it up,
|
|
257
|
+
so the extension asks Stripe for the refunds on that payment before recording them. **Check with
|
|
258
|
+
Stripe** on a payment's screen reconciles the same way, and will pick up a refund whose webhook was
|
|
259
|
+
missed entirely.
|
|
260
|
+
|
|
261
|
+
A refund amount larger than what is left is refused with the remaining amount in the payment's own
|
|
262
|
+
currency (`Only $5.00 is left to refund on this payment`).
|
|
263
|
+
|
|
264
|
+
## When an event cannot be applied
|
|
265
|
+
|
|
266
|
+
The webhook marks an event handled only **after** it has been applied. If applying it fails — the
|
|
267
|
+
database would not write — the endpoint answers with an error, the claim on that event is released,
|
|
268
|
+
and Stripe's retry is processed rather than being answered "duplicate" and dropped. Text that
|
|
269
|
+
contains a NUL byte (which Postgres cannot store) is refused with a 400 when a caller supplies it
|
|
270
|
+
and stripped when the processor does, since by then the money has already moved.
|
|
271
|
+
|
|
272
|
+
## Permissions
|
|
273
|
+
|
|
274
|
+
| Permission | Allows |
|
|
275
|
+
|---|---|
|
|
276
|
+
| `payments:view` | See payments, what they were for, and what happened to them |
|
|
277
|
+
| `payments:create` | Take a payment — start a new charge over the API |
|
|
278
|
+
| `payments:capture` | Capture or cancel a payment that was only authorised |
|
|
279
|
+
| `payments:refund` | Refund a payment, in full or in part |
|
|
280
|
+
| `payments:settings` | Change the payment provider and its API credentials |
|
|
281
|
+
|
|
282
|
+
Two groups ship with them: **`kempo-payments:operator`** handles payments day to day — looking them
|
|
283
|
+
up, capturing, refunding — without being able to see or change credentials, and
|
|
284
|
+
**`kempo-payments:administrator`** adds connecting the site to a processor.
|
|
285
|
+
|
|
286
|
+
`payments:create` is deliberately not held by an ordinary visitor. The HTTP route that starts a
|
|
287
|
+
payment takes the amount from the request body, so an open version of it would let anybody decide
|
|
288
|
+
what they owe. A customer-facing checkout computes the amount on its own server and calls the SDK.
|
|
289
|
+
|
|
290
|
+
## Settings
|
|
291
|
+
|
|
292
|
+
| Setting | Default | Meaning |
|
|
293
|
+
|---|---|---|
|
|
294
|
+
| `provider` | `stripe` | Which processor money goes through |
|
|
295
|
+
| `currency` | `usd` | Used when the caller does not name one |
|
|
296
|
+
| `capture_method` | `automatic` | See [Capture](#capture-automatic-or-manual) |
|
|
297
|
+
| `stripe_publishable_key` | — | `pk_test_…` / `pk_live_…`; public by design |
|
|
298
|
+
| `stripe_secret_key` | — | `sk_test_…` / `sk_live_…`; encrypted, never leaves the server |
|
|
299
|
+
| `stripe_webhook_secret` | — | `whsec_…`; without it, no payment is ever confirmed |
|
|
300
|
+
|
|
301
|
+
## Server SDK
|
|
302
|
+
|
|
303
|
+
```js
|
|
304
|
+
import {
|
|
305
|
+
createPayment, capturePayment, cancelPayment, refundPayment,
|
|
306
|
+
getPayment, getPaymentByRef, paymentsForReference, listPayments,
|
|
307
|
+
syncPayment, getClientSecret, refundableAmount, formatAmount,
|
|
308
|
+
} from 'kempo-payments/sdk';
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Every one returns kempo's `[error, data]` tuple. None of them checks a permission — the HTTP routes
|
|
312
|
+
do that, and anything importing this is server-side code that has already decided it is allowed.
|
|
313
|
+
|
|
314
|
+
`paymentsForReference(owner, reference)` is the lookup a consumer wants: it knows its order number,
|
|
315
|
+
not a payment id.
|
|
316
|
+
|
|
317
|
+
## HTTP API
|
|
318
|
+
|
|
319
|
+
Served under the extension's public scope, `/payments/`.
|
|
320
|
+
|
|
321
|
+
| Route | Permission | |
|
|
322
|
+
|---|---|---|
|
|
323
|
+
| `GET /payments/api/config` | none | Provider and publishable key, for the payment form |
|
|
324
|
+
| `GET /payments/api/payments` | `payments:view` | Filtered, paged list |
|
|
325
|
+
| `POST /payments/api/payments` | `payments:create` | Start a payment |
|
|
326
|
+
| `GET /payments/api/payments/:id` | `payments:view` | One payment, its refunds and its event log |
|
|
327
|
+
| `GET /payments/api/payments/:id/client-secret` | `payments:create` | Re-mount a form against a payment in flight |
|
|
328
|
+
| `POST /payments/api/payments/:id/capture` | `payments:capture` | |
|
|
329
|
+
| `POST /payments/api/payments/:id/cancel` | `payments:capture` | |
|
|
330
|
+
| `POST /payments/api/payments/:id/refund` | `payments:refund` | |
|
|
331
|
+
| `POST /payments/api/payments/:id/sync` | `payments:view` | Re-read from the processor |
|
|
332
|
+
| `GET /payments/api/summary` | `payments:view` | Counts and totals per currency |
|
|
333
|
+
| `GET`/`PUT /payments/api/settings` | `payments:settings` | Never returns a credential, only whether one is set |
|
|
334
|
+
| `POST /payments/api/webhooks/:provider` | signature | The processor's own endpoint |
|
|
335
|
+
|
|
336
|
+
## Amounts are integers
|
|
337
|
+
|
|
338
|
+
Always the currency's smallest unit. `4999` is $49.99. `1200` is ¥1,200, because yen have no minor
|
|
339
|
+
unit at all — and a Kuwaiti dinar has three decimal places, not two.
|
|
340
|
+
|
|
341
|
+
Floats are refused rather than converted. A caller passing `49.99` meant $49.99 and would be
|
|
342
|
+
charging five cents; the processor would accept that instruction without complaint. `toMinor()` is
|
|
343
|
+
exported for the places where a human genuinely typed a decimal into a box.
|
|
344
|
+
|
|
345
|
+
## Adding a provider
|
|
346
|
+
|
|
347
|
+
One file in `server/providers/`, exporting `name`, `label`, `webhookPath`, `webhookEvents`,
|
|
348
|
+
`signatureHeader`, `credentialFields`, and the functions `status`, `clientConfig`, `createIntent`,
|
|
349
|
+
`retrieveIntent`, `capture`, `cancel`, `refund`, `verifyEvent` and `interpretEvent`. Add its
|
|
350
|
+
credential settings to `kempo-config.json` and register it in `server/providers/index.js`.
|
|
351
|
+
|
|
352
|
+
The contract is documented in full at the top of `server/providers/index.js`. Statuses coming out of
|
|
353
|
+
it must be kempo's, not the processor's — mapping is the provider module's job, precisely so the
|
|
354
|
+
rest of the extension has one vocabulary rather than one per processor.
|
|
355
|
+
|
|
356
|
+
## What this does not do
|
|
357
|
+
|
|
358
|
+
- **Subscriptions and recurring billing.** A separate concern with its own state machine, dunning
|
|
359
|
+
and proration. This extension is the thing one would be built on.
|
|
360
|
+
- **Saved cards and stored customers.** Nothing is kept on file; every payment stands alone.
|
|
361
|
+
- **Payouts, balances and reconciliation.** The processor's dashboard is the ledger.
|
|
362
|
+
- **Answering disputes.** They are flagged here and answered there — evidence submission is a
|
|
363
|
+
processor-specific workflow with deadlines attached.
|
|
364
|
+
- **Tax.** That is `kempo-tax`, deliberately separate: "compute what is owed" and "move money" are
|
|
365
|
+
orthogonal, and merging them would stop anybody swapping in their own tax calculator.
|
|
366
|
+
- **A checkout.** No cart, no order, no idea what is being bought. That is `kempo-commerce`.
|
|
367
|
+
|
|
368
|
+
## Uninstalling
|
|
369
|
+
|
|
370
|
+
The tables are dropped, which loses this site's record of every payment it has taken. That record is
|
|
371
|
+
a copy — the charges themselves live at the processor and are entirely unaffected, and any of them
|
|
372
|
+
can still be refunded from its dashboard.
|
|
373
|
+
|
|
374
|
+
Uninstalling names any payment still authorised and uncaptured on the way out. Those are holds on
|
|
375
|
+
real cards that nothing will capture or release once the extension is gone.
|
|
376
|
+
|
|
377
|
+
## Development
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
npm install
|
|
381
|
+
npm run link:local # symlinks sibling kempo checkouts
|
|
382
|
+
docker compose up -d # test database on port 5439
|
|
383
|
+
DATABASE_URL=postgresql://kempo:kempo@localhost:5439/kempo_payments_test npx drizzle-kit push --force
|
|
384
|
+
npm test
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
The database suites (data layer, lifecycle, webhooks) **skip themselves** when no database is
|
|
388
|
+
reachable, and a skip reads as a pass — check for `(SKIPPED)` before believing a green run covered
|
|
389
|
+
them. They also skip themselves when `DATABASE_URL` names a database that does not end in `_test`,
|
|
390
|
+
because they **empty the payment tables** first: pointing one at a real site's database would
|
|
391
|
+
delete its payments. The webhook suite additionally needs `SETTINGS_ENCRYPTION_KEY` (any 64 hex
|
|
392
|
+
characters will do) to store the signing secret it verifies against.
|
|
393
|
+
|
|
394
|
+
No Stripe account is needed for any test: every provider response in the suite is a literal, and
|
|
395
|
+
webhook events are signed with Stripe's own test-signature helper, because a suite that needs an
|
|
396
|
+
account is a suite nobody runs.
|
|
397
|
+
|
|
398
|
+
## License
|
|
399
|
+
|
|
400
|
+
MIT
|