# @backendfree/payments

> Taking a payment into the business's own account, from your own server with a secret key.

A project's money: what has been taken, and taking some. Built on
[`@backendfree/core`](../core).

```sh
npm install @backendfree/core @backendfree/payments
```

```ts
import { Client } from '@backendfree/core';
import { Payments } from '@backendfree/payments';

const payments = new Payments(
  new Client({ origin: 'https://backendfree.com', key: process.env.BACKENDFREE_SECRET_KEY! }),
);

const payment = await payments.checkout({
  amount: 3500,                              // minor units: CHF 35.00
  description: 'Deposit',
  success_url: 'https://shop.example/thanks?payment={id}',
  cancel_url: 'https://shop.example/cart',
  metadata: { order: 'A-1042' },             // your own references, returned on every read and event
});

redirect(payment.checkout_url);
```

`{id}` in either address becomes the payment's id, which does not exist until
this call returns, so the page after paying can read the payment it was for.
`metadata` comes back on the payment and in every `payment.*` event about it,
so the webhook can find the order it paid for: at most 20 keys, each value text.

## Server only

Every method here needs a secret key, and constructing `Payments` with a
publishable one throws. A payment carries the paying customer's name and email,
and a checkout is a charge on a bank account, so none of it belongs in a bundle
whoever loads the site can read.

That is also why the mistake is refused at construction rather than by a 403 in
production: the stack trace points at the line that is wrong.

## Why there is no refund method

Refunding is the one act that moves money out of the business's own account, so
it lives in the payments screen in the dashboard, behind a capability only the
account owner holds.

A secret key lives in server code, deploy settings and CI, which is further than
the account owner's own screen. A leaked key that can read payment records is one
incident; one that could refund would drain the account. Reading payment records
with a key is deliberate and bounded, so a developer can render a receipt;
issuing money back is not the same thing and is not in the API.

## Nothing here says a payment was made

A checkout is a page that exists. The provider's webhook is what says money
moved, and the page the customer lands on afterwards says nothing at all.

Take the platform's `payment.succeeded` webhook and verify it:

```ts
import { verifyWebhook } from '@backendfree/core';

const event = await verifyWebhook({ body: await request.text(), headers, secret });
if (event.name === 'payment.succeeded') {
  markPaid(event.data.metadata.order, event.data.amount, event.data.currency);
}
```

## When the provider refuses

`checkout()` does not throw for a provider that would not open a page. The
payment comes back with an empty `checkout_url` and the reason on it, so the
attempt is on record and you have an id to try again with.

```ts
if (!payment.checkout_url) {
  payment = await payments.offerAgain(payment.id, { success_url, cancel_url });
}
```

`offerAgain` is keyed on the payment, so asking twice is one page, and what is
owed comes off the payment rather than off the call. Calling `checkout()` a
second time would record a second payment for one order, and a customer handed
both pages could pay twice.

## Reading what has been taken

```ts
await payments.list({ status: 'paid' });
await payments.payment(id);
await payments.forSubject('bookings.booking', bookingId);   // was it paid for?
```

A payment says what it settled, how much, what has gone back and what is left to
give back. It never says which provider account the money landed in: that is the
business's own and appears nowhere in this API.

`refundable` is published rather than left to you to work out, because the
arithmetic is not what it looks like. A refund that is still settling is money
already spent, so subtracting only the settled ones shows more left than there
is.

## Money

Always minor units and a currency. `3500` with `CHF` is 35 francs.

A checkout takes a currency counted in hundredths, the same ones a project's
currency is chosen from on its settings screen, and leaving `currency` out uses
the project's own. Anything else (JPY, KWD and ISK among them) throws with the
code `invalid_currency` and records no payment:

```ts
import { BackendFreeError } from '@backendfree/core';

try {
  await payments.checkout({ amount: 500, currency: 'JPY', success_url, cancel_url });
} catch (error) {
  if (error instanceof BackendFreeError && error.code === 'invalid_currency') {
    // nothing was recorded: send a currency the project can be priced in
  }
}
```

To show an amount, `formatMoney` reads the places from the currency rather
than from a constant, so it is right for any code, including ones a checkout
does not take:

```ts
import { formatMoney } from '@backendfree/payments';

formatMoney({ amount: 6500, currency: 'CHF' }, 'de-CH');   // CHF 65.00
formatMoney({ amount: 6500, currency: 'JPY' }, 'en-US');   // ¥6,500
```

Dividing by a hundred yourself is wrong in both directions: JPY has no minor
unit and KWD has three places.

## Reading a status

```ts
import { hasPaid, isOver } from '@backendfree/payments';

hasPaid(payment);   // the money arrived, refunded payments included
isOver(payment);    // nothing further will happen, so do not offer a page
```

A status only ever moves forward. A provider redelivers events for days and
promises no order, so the platform applies each one as a maximum and a late
"still pending" cannot undo a payment that arrived.
