# Payment method plugins

> Add any payment provider with an API. The plugin starts payments and reads callbacks; ShopFast keeps the amounts, the ledger and the order.

Source: https://shopfa.st/docs/plugins/payment

A payment plugin is the seller's own gateway (stores on **their own gateway** model; under ShopFast Pay, ShopFast
collects every payment and plugins are not used). The platform's `plugin` gateway switch in the admin panel decides
whether payment plugins may run at all.

## manifest.json

```json
{
  "name": "Example Pay",
  "version": "1.0.0",
  "kind": "payment",
  "permissions": { "http": ["api.example-pay.com"], "kv": false },
  "settings": [
    { "key": "api_key", "label": "API key", "type": "secret", "required": true },
    { "key": "webhook_secret", "label": "Webhook signing secret", "type": "secret", "required": true }
  ],
  "payment": { "label": "Card (Example Pay)", "methods": ["card"] }
}
```

`payment.methods` are the methods buyers can pick at checkout (`card`, `crypto`, `bank_transfer`, `paypal`, `manual`,
`apple_pay`, `google_pay`).

## create_payment

Called when the buyer places the order. Start the payment with your provider and return where to send the buyer and
your provider's reference.

```lua
function plugin.create_payment(ctx, input)
  local s = input.session
  local res, err = http.fetch({
    url = "https://api.example-pay.com/v1/payments", method = "POST",
    headers = { authorization = "Bearer " .. ctx.settings.api_key },
    body = { amount = s.amountMinor, currency = s.currency, reference = s.id,
             success_url = s.returnUrl, cancel_url = s.cancelUrl, notify_url = s.callbackUrl },
  })
  if not res then error(err) end
  if not res.ok then error("Example Pay answered " .. res.status) end
  local p = res.json()
  return { redirect_url = p.checkout_url, provider_ref = p.id }
end
```

`input.session`: `id`, `amountMinor`, `currency`, `method`, `buyerEmail`, `items` (`name`, `quantity`, `unitMinor`),
`returnUrl`, `cancelUrl`, `callbackUrl` and `expiresAt`. Give `callbackUrl` to the provider as its webhook or
notification URL. If the hook fails, the buyer sees "This payment method is not available right now" and can pick
another way to pay.

## verify_callback

The provider calls `callbackUrl` (`/v1/plugins/plg_…/callback`, GET or POST). The plugin gets the raw request and
decides what happened. **Verify the provider's signature first.**

```lua
function plugin.verify_callback(ctx, request)
  local expected = crypto.hmac("sha256", ctx.settings.webhook_secret, request.body)
  if not crypto.equal(request.headers["x-signature"] or "", expected) then
    return { status = "ignore", reply = "bad signature" }
  end
  local e = json.decode(request.body)
  if e.type ~= "payment.succeeded" then return { status = "ignore" } end
  return { status = "paid", provider_ref = e.payment.id, event_id = e.id,
           amount_minor = e.payment.amount, currency = e.payment.currency }
end
```

`request`: `method`, `headers` (lowercase names; cookies and `authorization` removed), `query` and `body`. Return
`status` = `paid`, `pending`, `failed` or `ignore`, and for `paid` also `provider_ref`, `event_id`, `amount_minor` and
`currency`. `reply` is the body ShopFast answers the provider with (`ok` by default).

## What ShopFast checks for you

A `paid` answer becomes a gateway event and goes through the same rules as Stripe or BTCPay:

- the payment must be one **this plugin** created for **this store** (`provider_ref` is matched against the checkout);
- `amount_minor` and `currency` must equal the checkout total exactly, or the payment is refused and the run is marked
  failed with the reason;
- each `event_id` is applied once, so provider retries are harmless;
- fees, the ledger, fulfillment and webhooks then run as for any other payment.

A plugin can say a payment happened, but it can never pay a different checkout, another store's order or a lower
amount. Refunds of plugin payments are made at the provider; ShopFast records them and reverses the ledger.
