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
7 changes: 6 additions & 1 deletion docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ export default defineConfig({
{ text: 'Standalone (no Laravel)', link: '/guide/standalone' },
],
},
{
text: 'Migration',
items: [
{ text: 'From sveaekonomi/checkout', link: '/guide/migration-from-official-sdk' },
],
},
{
text: 'Advanced',
items: [
Expand Down Expand Up @@ -119,4 +125,3 @@ export default defineConfig({
},
},
})

9 changes: 4 additions & 5 deletions docs/guide/fluent-builders.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,15 @@ Svea::admin()

## Both styles in tests

Inside `Svea::fake()` callbacks the builders are passed pre-constructed, so test code typically uses the fluent style. See the [Testing & Fakes guide](./testing).
Inside `Svea::fake()` callbacks the builders are passed pre-constructed, so test code typically uses the fluent style. See the [Testing & Fakes guide](./testing.md).

## When the styles diverge

Most builders cover both styles 1:1. A few details to know:

- **`OrderRow` (Checkout)** applies an SDK-level × 100 conversion for `quantity`, `vatPercent`, and `discountPercent`. Pass human values (`1`, `25`, `10`).
- **`AdminOrderRow` (Admin)** does *not* convert — pass already-scaled minor-unit values (`100`, `2500`, `1000`).
- **`OrderRow` (Checkout)** uses Svea's native minor-unit format. Pass already-scaled values (`100` = 1 item, `2500` = 25%, `1000` = 10%).
- **`AdminOrderRow` (Admin)** uses the same minor-unit format for row mutations. Pass already-scaled values (`100`, `2500`, `1000`).
- **`replaceOrderRows()`** is variadic: pass one closure per replacement row, no array wrapping needed.
- **`CreditRequest`** chains state — call `rows([...])` *and/or* one or more `newRow(...)` callbacks before `->send()`.

See the [Checkout](../api/checkout) and [Admin](../api/admin) reference pages for surface-specific examples.

See the [Checkout](../api/checkout.md) and [Admin](../api/admin.md) reference pages for surface-specific examples.
355 changes: 355 additions & 0 deletions docs/guide/migration-from-official-sdk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
# Migration from `sveaekonomi/checkout`

This guide helps existing projects move from Svea's older official PHP Checkout
SDK, [`sveaekonomi/checkout`](https://github.com/sveawebpay/php-checkout), to
`nordkit/svea`.

The high-level migration is:

1. replace the old connector/client setup with package configuration;
2. convert array payloads to typed builders or fluent callbacks;
3. move post-checkout operations from `CheckoutAdminClient` to `Svea::admin()`;
4. add webhook verification through `Svea::webhook()` or `Webhook::constructEvent()`;
5. cover the migrated flows with `Svea::fake()` tests.

## Why migrate

- PHP 8.2+ support with strict types and modern value objects.
- Fluent builders for checkout, admin, subscription, and webhook flows.
- Laravel-native service provider, facade, config publishing, and test fakes.
- Typed exceptions such as `SveaApiException` and `SignatureVerificationException`.
- Framework-agnostic core for non-Laravel applications.
- Built-in retry, timeout, idempotency, and fake-client testing support.

## Install and configure

Replace the old package:

```bash
composer remove sveaekonomi/checkout
composer require nordkit/svea
```

For Laravel applications, publish the config file:

```bash
php artisan vendor:publish --tag=svea-config
```

Then move the old connector values into environment variables:

| Old SDK value | New config / environment |
| --- | --- |
| `checkoutMerchantId` passed to `Connector::init(...)` | `SVEA_MERCHANT_ID` |
| `checkoutSecret` passed to `Connector::init(...)` | `SVEA_SHARED_SECRET` |
| `Connector::TEST_BASE_URL` / `TEST_ADMIN_BASE_URL` | `SVEA_ENVIRONMENT=test` |
| `Connector::PROD_BASE_URL` / `PROD_ADMIN_BASE_URL` | `SVEA_ENVIRONMENT=production` |
| custom checkout base URL | `SVEA_CHECKOUT_URL` |
| custom admin base URL | `SVEA_ADMIN_URL` |
| API timeout constant | `SVEA_TIMEOUT` |
| application-level webhook secret (no old SDK built-in equivalent) | `SVEA_WEBHOOK_SECRET` |

See [Configuration](./configuration.md) for the full `config/svea.php` shape.

## Side-by-side cheat sheet

### Creating a checkout order

Old SDK:

```php
$connector = \Svea\Checkout\Transport\Connector::init(
$checkoutMerchantId,
$checkoutSecret,
\Svea\Checkout\Transport\Connector::TEST_BASE_URL,
);

$client = new \Svea\Checkout\CheckoutClient($connector);

$response = $client->create([
'countryCode' => 'SE',
'currency' => 'SEK',
'locale' => 'sv-SE',
'clientOrderNumber' => 'ORD-001',
'merchantSettings' => [
'termsUri' => 'https://example.com/terms',
'checkoutUri' => 'https://example.com/checkout',
'confirmationUri' => 'https://example.com/checkout/confirm',
'pushUri' => 'https://example.com/webhooks/svea?order={checkout.order.uri}',
],
'cart' => [
'items' => [
[
'articleNumber' => 'SKU-1',
'name' => 'T-Shirt',
'quantity' => 100,
'unitPrice' => 29900,
'vatPercent' => 2500,
'unit' => 'st',
],
],
],
]);

$orderId = $response['OrderId'];
$snippet = $response['Gui']['Snippet'];
```

New SDK:

```php
use Svea\Checkout\CheckoutOrder;
use Svea\Checkout\MerchantSettings;
use Svea\Checkout\OrderRow;

$response = Svea::checkout()->create(function (CheckoutOrder $order) {
$order
->countryCode('SE')
->currency('SEK')
->locale('sv-SE')
->clientOrderNumber('ORD-001')
->merchantSettings(fn (MerchantSettings $settings) => $settings
->termsUri('https://example.com/terms')
->checkoutUri('https://example.com/checkout')
->confirmationUri('https://example.com/checkout/confirm')
->pushUri('https://example.com/webhooks/svea?order={checkout.order.uri}'))
->addRow(fn (OrderRow $row) => $row
->sku('SKU-1')
->name('T-Shirt')
->quantity(100)
->unitPrice(29900)
->vatPercent(2500)
->unit('st'));
});

$orderId = $response->id();
$snippet = $response->snippet();
```

### Getting a checkout order

Old SDK:

```php
$response = $client->get(['orderId' => $orderId]);
$status = $response['Status'];
$snippet = $response['Gui']['Snippet'];
```

New SDK:

```php
$response = Svea::checkout()->get($orderId);
$status = $response->status();
$snippet = $response->snippet();
```

### Updating cart rows

Old SDK:

```php
$response = $client->update([
'orderId' => $orderId,
'merchantData' => 'updated cart',
'cart' => [
'items' => [
[
'articleNumber' => 'SKU-1',
'name' => 'Updated T-Shirt',
'quantity' => 200,
'unitPrice' => 29900,
'vatPercent' => 2500,
],
],
],
]);
```

New SDK:

```php
$response = Svea::checkout()->update($orderId, function (CheckoutOrder $order) {
$order
->merchantData('updated cart')
->addRow(fn (OrderRow $row) => $row
->sku('SKU-1')
->name('Updated T-Shirt')
->quantity(200)
->unitPrice(29900)
->vatPercent(2500));
});
```

### Delivering an order

Old SDK:

```php
$admin = new \Svea\Checkout\CheckoutAdminClient($adminConnector);

$response = $admin->deliverOrder([
'orderId' => $orderId,
'orderRowIds' => [],
]);
```

New SDK:

```php
$response = Svea::admin()->order($orderId)->deliver();

// Partial delivery:
$response = Svea::admin()->order($orderId)->deliver(rows: [101, 102]);

$taskUrl = $response->taskReference();
```

### Crediting / refunding

Old SDK:

```php
$response = $admin->creditOrderAmount([
'orderId' => $orderId,
'deliveryId' => $deliveryId,
'creditedAmount' => 2000,
]);

$response = $admin->creditOrderRows([
'orderId' => $orderId,
'deliveryId' => $deliveryId,
'orderRowIds' => [101, 102],
]);
```

New SDK:

```php
Svea::admin()
->order($orderId)
->delivery($deliveryId)
->creditAmount(2000);

Svea::admin()
->order($orderId)
->delivery($deliveryId)
->credit()
->rows([101, 102])
->send();
```

### Cancelling

Old SDK:

```php
$admin->cancelOrder([
'orderId' => $orderId,
'IsCancelled' => true,
]);

$admin->cancelOrderAmount([
'orderId' => $orderId,
'cancelledAmount' => 5000,
]);

$admin->cancelOrderRow([
'orderId' => $orderId,
'orderRowId' => 101,
]);
```

New SDK:

```php
Svea::admin()->order($orderId)->cancel();
Svea::admin()->order($orderId)->cancelAmount(5000);
Svea::admin()->order($orderId)->cancelRow(101);
```

### Verifying webhook signatures

Old SDK projects often handled webhook verification in application code around
the push endpoint.

New SDK, Laravel:

```php
use Svea\Exceptions\SignatureVerificationException;

try {
$event = Svea::webhook()->fromRequest($request);
} catch (SignatureVerificationException) {
abort(400);
}
```

New SDK, framework-agnostic:

```php
use Svea\Webhooks\Webhook;

$event = Webhook::constructEvent(
payload: file_get_contents('php://input'),
signature: $_SERVER['HTTP_SVEA_SIGNATURE'] ?? '',
secret: getenv('SVEA_WEBHOOK_SECRET'),
);
```

## Response mapping

The old SDK returns arrays. The new SDK wraps responses in small helper objects:

| Old array access | New helper |
| --- | --- |
| `$response['OrderId']` | `$response->id()` |
| `$response['Gui']['Snippet']` | `$response->snippet()` |
| `$response['Status']` | `$response->status()` |
| admin order `Actions` array checks | `$adminOrder->canDeliver()`, `canCredit()`, `canCancel()` |
| async task header / location | `$response->taskReference()` |

## Gotchas

- **Minor units stay important.** Checkout and Admin examples use Svea's minor
units (`100` = one quantity unit, `2500` = 25% VAT, `29900` = 299.00 SEK).
- **Checkout and Admin row builders differ.** `OrderRow` is for checkout order
creation/update. `AdminOrderRow` is for post-checkout row mutations.
- **Admin operations may be asynchronous.** Deliver, credit, and some row
mutations can return a task reference. Poll it with `Svea::admin()->task($url)`
in a queue/job instead of blocking the checkout request.
- **Check available actions before mutating final orders.** Use
`Svea::admin()->order($id)->get()` and helpers such as `canDeliver()` before
calling delivery, credit, or cancel operations.
- **Cancel is a `PATCH` operation in Svea Payment Admin.** Use the SDK method
(`cancel()`, `cancelAmount()`, or `cancelRow()`) instead of hard-coding HTTP
verbs in migrated code.
- **Exception handling changes.** Replace old `SveaInputValidationException`,
`SveaConnectorException`, and generic array error handling with the
`Svea\Exceptions\*` hierarchy documented in [Error Handling](./error-handling.md).
- **Webhook secret is separate.** `SVEA_WEBHOOK_SECRET` verifies inbound
webhook signatures. It is not the same as `SVEA_SHARED_SECRET`, which signs
outbound API calls.

## Testing the migrated flow

For Laravel applications, replace live checkout calls in tests with
`Svea::fake()`:

```php
use Svea\Checkout\CheckoutResponse;

Svea::fake([
'checkout.create' => CheckoutResponse::make([
'OrderId' => '12345678',
'Gui' => ['Snippet' => '<div id="svea-checkout"></div>'],
]),
]);

// Run your checkout action...

Svea::assertCheckoutCreated();
```

See [Testing & Fakes](./testing.md) for admin, task, subscription, and
`preventStrayRequests()` examples.
Loading