includio-cms 0.15.0 → 0.15.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/CHANGELOG.md +34 -0
- package/DOCS.md +231 -1
- package/ROADMAP.md +2 -2
- package/dist/admin/client/shop/shipping-method-edit-page.svelte +1 -0
- package/dist/admin/client/shop/shipping-method-form.svelte +65 -0
- package/dist/admin/client/shop/shipping-method-form.svelte.d.ts +7 -0
- package/dist/admin/client/shop/shipping-method-new-page.svelte +1 -0
- package/dist/admin/remote/shop.remote.d.ts +16 -0
- package/dist/admin/remote/shop.remote.js +3 -1
- package/dist/cli/scaffold/admin.js +32 -0
- package/dist/db-postgres/schema/shop/order.d.ts +34 -0
- package/dist/db-postgres/schema/shop/order.js +2 -0
- package/dist/db-postgres/schema/shop/shippingMethod.d.ts +19 -0
- package/dist/db-postgres/schema/shop/shippingMethod.js +1 -0
- package/dist/paraglide/messages/_index.d.ts +3 -36
- package/dist/paraglide/messages/_index.js +3 -71
- package/dist/paraglide/messages/hello_world.d.ts +5 -0
- package/dist/paraglide/messages/hello_world.js +33 -0
- package/dist/paraglide/messages/login_hello.d.ts +16 -0
- package/dist/paraglide/messages/login_hello.js +34 -0
- package/dist/paraglide/messages/login_please_login.d.ts +16 -0
- package/dist/paraglide/messages/login_please_login.js +34 -0
- package/dist/shop/adapters/payu/client.d.ts +22 -0
- package/dist/shop/adapters/payu/client.js +78 -0
- package/dist/shop/adapters/payu/index.d.ts +24 -0
- package/dist/shop/adapters/payu/index.js +88 -0
- package/dist/shop/adapters/payu/payload.d.ts +48 -0
- package/dist/shop/adapters/payu/payload.js +48 -0
- package/dist/shop/adapters/payu/signature.d.ts +12 -0
- package/dist/shop/adapters/payu/signature.js +50 -0
- package/dist/shop/adapters/payu/status-map.d.ts +3 -0
- package/dist/shop/adapters/payu/status-map.js +14 -0
- package/dist/shop/cart/order-token-cookie.d.ts +9 -0
- package/dist/shop/cart/order-token-cookie.js +40 -0
- package/dist/shop/client/index.d.ts +63 -1
- package/dist/shop/client/index.js +9 -0
- package/dist/shop/client/use-order.svelte.d.ts +32 -0
- package/dist/shop/client/use-order.svelte.js +105 -0
- package/dist/shop/http/checkout-handler.js +47 -4
- package/dist/shop/http/index.d.ts +4 -0
- package/dist/shop/http/index.js +4 -0
- package/dist/shop/http/order-handler.d.ts +4 -0
- package/dist/shop/http/order-handler.js +85 -0
- package/dist/shop/http/refresh-payment-handler.d.ts +4 -0
- package/dist/shop/http/refresh-payment-handler.js +73 -0
- package/dist/shop/http/retry-payment-handler.d.ts +4 -0
- package/dist/shop/http/retry-payment-handler.js +99 -0
- package/dist/shop/http/retry-payment-logic.d.ts +2 -0
- package/dist/shop/http/retry-payment-logic.js +4 -0
- package/dist/shop/http/shipping-handler.js +2 -1
- package/dist/shop/http/webhook-handler.d.ts +4 -0
- package/dist/shop/http/webhook-handler.js +73 -0
- package/dist/shop/http/webhook-logic.d.ts +4 -0
- package/dist/shop/http/webhook-logic.js +21 -0
- package/dist/shop/index.d.ts +3 -1
- package/dist/shop/index.js +3 -1
- package/dist/shop/server/email.js +18 -2
- package/dist/shop/server/order-access-url.d.ts +7 -0
- package/dist/shop/server/order-access-url.js +6 -0
- package/dist/shop/server/orders.d.ts +1 -0
- package/dist/shop/server/orders.js +12 -0
- package/dist/shop/server/payment-compat.d.ts +5 -0
- package/dist/shop/server/payment-compat.js +9 -0
- package/dist/shop/server/shipping.d.ts +1 -0
- package/dist/shop/server/shipping.js +3 -0
- package/dist/shop/svelte/OrderStatus.svelte +368 -0
- package/dist/shop/svelte/OrderStatus.svelte.d.ts +14 -0
- package/dist/shop/svelte/index.d.ts +3 -0
- package/dist/shop/svelte/index.js +2 -0
- package/dist/shop/svelte/labels.d.ts +25 -0
- package/dist/shop/svelte/labels.js +41 -0
- package/dist/shop/types.d.ts +19 -1
- package/dist/updates/0.15.1/index.d.ts +2 -0
- package/dist/updates/0.15.1/index.js +27 -0
- package/dist/updates/index.js +2 -1
- package/package.json +5 -1
- package/dist/paraglide/messages/en.d.ts +0 -5
- package/dist/paraglide/messages/en.js +0 -14
- package/dist/paraglide/messages/pl.d.ts +0 -5
- package/dist/paraglide/messages/pl.js +0 -14
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,40 @@
|
|
|
3
3
|
All notable changes to includio-cms are documented here.
|
|
4
4
|
Generated from `src/lib/updates/` — do not edit manually.
|
|
5
5
|
|
|
6
|
+
## 0.15.1 — 2026-04-15
|
|
7
|
+
|
|
8
|
+
Shop: PayU payment adapter + secure order access (token-gated view API, email link).
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Orders now carry an `accessToken` — public order-view API is gated by this token (no enumeration by order number).
|
|
12
|
+
- New `createOrderHandler()` in `includio-cms/shop/http` — `GET /api/shop/orders/[number]?token=...` returns order + items + status history; accepts cookie fallback `aria_shop_order` written on checkout (30-min httpOnly).
|
|
13
|
+
- `ShopConfig.orderViewUrl` template — placeholders `{orderNumber}`, `{orderId}`, `{accessToken}`, `{language}`. Default: `/shop/order/{orderNumber}?token={accessToken}`. When set to an absolute URL, status emails include a "View order" button.
|
|
14
|
+
- New `createPaymentWebhookHandler()` — mount at `/api/shop/webhooks/[provider]/+server.ts`. Dispatches to the matching `PaymentAdapter.handleWebhook`, maps payment event to order status, and is idempotent (terminal orders ack 200 no-op — safe against provider retries). 400 on bad signature, 200 on unknown order.
|
|
15
|
+
- Checkout now calls `PaymentAdapter.createPayment()` after the order is created. The response exposes `paymentStatus`, `requiresPaymentRedirect`, `redirectUrl`, and `accessToken` so the frontend can redirect to the gateway and deep-link back to the order view.
|
|
16
|
+
- Orders store `paymentProviderRef` — the external id returned by the payment adapter (e.g. PayU orderId). Used to correlate webhooks and future refunds/status polls.
|
|
17
|
+
- New `payuAdapter()` — OAuth token caching, create order (REST v2.1), MD5 `OpenPayU-Signature` verification on webhooks, full PayU status mapping (including REJECTED and WAITING_FOR_CONFIRMATION). Import from `includio-cms/shop`.
|
|
18
|
+
- `PaymentAdapter.createPayment()` receives an optional `{ customerIp, language }` context, threaded from the SvelteKit request in the built-in checkout handler.
|
|
19
|
+
- Shipping ↔ payment compatibility: `shop_shipping_methods.allowed_payment_methods` (jsonb string[] | null). Null/empty = any payment method allowed; otherwise checkout rejects the order when the chosen payment is not in the list (e.g. disable COD for paczkomat). The shipping-methods API exposes the list so the frontend can filter payment options per shipping choice.
|
|
20
|
+
- Admin: shipping method form now exposes an "allowed payment methods" section with a restrict toggle + multi-select of configured payment adapters. `getShopConfig` remote returns the payment adapter list (id + i18n label).
|
|
21
|
+
- New optional `PaymentAdapter.getStatus(providerRef)` for provider-side polling, plus `createRefreshPaymentHandler()` mounted at `/api/shop/orders/[number]/refresh-payment`. Token-gated (same as order view). Used as a safety net when a webhook is lost — PayU adapter implements it out of the box.
|
|
22
|
+
- Retry payment — `createRetryPaymentHandler()` at `POST /api/shop/orders/[number]/retry-payment`. Reuses the same order, creates a fresh payment session via the adapter, stores the new `paymentProviderRef`. Status `paymentRejected` is rolled back to `awaitingPayment`. Terminal states return 409.
|
|
23
|
+
- Shop SDK: new `orders` namespace on `createShopClient()` — `get(number, token)`, `refreshPayment(number, token)`, `retryPayment(number, token)`. All response types exported from `includio-cms/shop/client`.
|
|
24
|
+
- Headless Svelte 5 helper `createOrderState({ number, token, initialData? })` — reactive state class with `data/loading/error/phase`, `load()`, `refreshPayment()`, `retry()`, `startPolling()/stopPolling()`. Supports SSR via `initialData`.
|
|
25
|
+
- Generic `<OrderStatus />` Svelte component — drop-in order view with awaiting/success/rejected states, auto-polling, retry button, status timeline. Full theming via CSS custom properties and `::part()`. Import from `includio-cms/shop/svelte`.
|
|
26
|
+
- Docs: new section covering the shop module, order view patterns (generic vs headless), and retry lifecycle.
|
|
27
|
+
|
|
28
|
+
### Migration
|
|
29
|
+
|
|
30
|
+
```sql
|
|
31
|
+
ALTER TABLE shop_orders ADD COLUMN access_token uuid NOT NULL DEFAULT gen_random_uuid();
|
|
32
|
+
ALTER TABLE shop_orders ADD COLUMN payment_provider_ref text;
|
|
33
|
+
ALTER TABLE shop_shipping_methods ADD COLUMN allowed_payment_methods jsonb;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Notes
|
|
37
|
+
|
|
38
|
+
Requires `gen_random_uuid()` (pgcrypto or Postgres 13+). Existing orders get a fresh random token on migration — pre-existing customer links keep working because order view links were not shipped before this version.
|
|
39
|
+
|
|
6
40
|
## 0.15.0 — 2026-04-14
|
|
7
41
|
|
|
8
42
|
Shop module MVP — headless e-commerce: products, cart, orders, payments, emails
|
package/DOCS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Includio CMS Documentation (v0.15.
|
|
1
|
+
# Includio CMS Documentation (v0.15.1)
|
|
2
2
|
|
|
3
3
|
> This file is auto-generated from the docs site. For the latest version, update the package.
|
|
4
4
|
|
|
@@ -4297,6 +4297,236 @@ Options for `create`/`createAndPublish`: `{ skipValidation?, sortOrder?, lang? }
|
|
|
4297
4297
|
> **Delete Restrictions:** Only archived entries can be permanently deleted. Call `archive` first, then `delete`.
|
|
4298
4298
|
|
|
4299
4299
|
|
|
4300
|
+
---
|
|
4301
|
+
|
|
4302
|
+
# Shop
|
|
4303
|
+
|
|
4304
|
+
Headless e-commerce module. Optional — activate by adding `shop: defineShop({...})` to your CMS config.
|
|
4305
|
+
|
|
4306
|
+
> Shop is in beta. API may change between 0.15.x patches.
|
|
4307
|
+
|
|
4308
|
+
## What you get
|
|
4309
|
+
|
|
4310
|
+
- **Products as entries** — add `{ type: 'shop', slug: 'shop' }` to any collection; its entries become purchasable.
|
|
4311
|
+
- **Cart** — signed cookie, headless SDK (`createShopClient()`), `POST/PATCH/DELETE /api/shop/cart`.
|
|
4312
|
+
- **Checkout + orders** — `POST /api/shop/checkout` creates an order with consent validation, stock reservation (30-min TTL), and status emails.
|
|
4313
|
+
- **Payment adapters** — `manualAdapter()` (bank transfer / COD) and `payuAdapter()` ship built-in; plug your own by implementing `PaymentAdapter`.
|
|
4314
|
+
- **Webhook infrastructure** — `createPaymentWebhookHandler()` dispatches provider callbacks, verifies signatures, is idempotent.
|
|
4315
|
+
- **Secure order view** — per-order `accessToken` + cookie fallback + `GET /api/shop/orders/[number]` token-gated API.
|
|
4316
|
+
- **Shipping ↔ payment compatibility** — restrict payment methods per shipping (e.g. no COD for paczkomat).
|
|
4317
|
+
|
|
4318
|
+
## Config
|
|
4319
|
+
|
|
4320
|
+
```ts
|
|
4321
|
+
import { defineShop, manualAdapter, payuAdapter } from 'includio-cms/shop';
|
|
4322
|
+
|
|
4323
|
+
export const cmsConfig = defineCMS({
|
|
4324
|
+
shop: defineShop({
|
|
4325
|
+
currency: 'PLN',
|
|
4326
|
+
vatRates: [23, 8, 0],
|
|
4327
|
+
orderViewUrl: `${process.env.PUBLIC_URL}/shop/order/{orderNumber}?token={accessToken}`,
|
|
4328
|
+
payment: [
|
|
4329
|
+
manualAdapter({ id: 'transfer', label: { pl: 'Przelew', en: 'Bank transfer' } }),
|
|
4330
|
+
payuAdapter({
|
|
4331
|
+
posId: process.env.PAYU_POS_ID!,
|
|
4332
|
+
clientId: process.env.PAYU_CLIENT_ID!,
|
|
4333
|
+
clientSecret: process.env.PAYU_CLIENT_SECRET!,
|
|
4334
|
+
secondKey: process.env.PAYU_SECOND_KEY!,
|
|
4335
|
+
environment: 'sandbox',
|
|
4336
|
+
notifyUrl: `${process.env.PUBLIC_URL}/api/shop/webhooks/payu`
|
|
4337
|
+
})
|
|
4338
|
+
],
|
|
4339
|
+
features: { variants: true, stock: true },
|
|
4340
|
+
consents: [{ id: 'terms', required: true, labelI18n: { pl: 'Akceptuję regulamin', en: 'I accept terms' } }]
|
|
4341
|
+
})
|
|
4342
|
+
})
|
|
4343
|
+
```
|
|
4344
|
+
|
|
4345
|
+
## Routes
|
|
4346
|
+
|
|
4347
|
+
Scaffold provisions these endpoints in your app:
|
|
4348
|
+
|
|
4349
|
+
| Path | Handler | Purpose |
|
|
4350
|
+
|------|---------|---------|
|
|
4351
|
+
| `POST /api/shop/cart` | `createCartHandler()` | Cart CRUD |
|
|
4352
|
+
| `GET /api/shop/shipping-methods` | `createShippingMethodsHandler()` | List active shipping |
|
|
4353
|
+
| `POST /api/shop/checkout` | `createCheckoutHandler()` | Create order + initiate payment |
|
|
4354
|
+
| `GET /api/shop/orders/[number]` | `createOrderHandler()` | Token-gated order view |
|
|
4355
|
+
| `POST /api/shop/orders/[number]/refresh-payment` | `createRefreshPaymentHandler()` | Pull status from provider |
|
|
4356
|
+
| `POST /api/shop/orders/[number]/retry-payment` | `createRetryPaymentHandler()` | New payment attempt |
|
|
4357
|
+
| `POST /api/shop/webhooks/[provider]` | `createPaymentWebhookHandler()` | Provider callbacks |
|
|
4358
|
+
|
|
4359
|
+
## See also
|
|
4360
|
+
|
|
4361
|
+
- [Order view](/docs/shop/order-view) — generic `<OrderStatus>` component + headless helper
|
|
4362
|
+
- [Retry payment](/docs/shop/retry-payment) — lifecycle + endpoint
|
|
4363
|
+
|
|
4364
|
+
|
|
4365
|
+
---
|
|
4366
|
+
|
|
4367
|
+
# Order view
|
|
4368
|
+
|
|
4369
|
+
After checkout the customer needs a page that shows their order status, payment outcome, and retry options. Shop offers two paths — a batteries-included component for quick projects, and a headless helper for custom designs.
|
|
4370
|
+
|
|
4371
|
+
## 1. Generic component (`<OrderStatus />`)
|
|
4372
|
+
|
|
4373
|
+
Drop-in view with three visual states (awaiting payment / success / rejected), auto-polling, retry button, status timeline. Theme through CSS custom properties.
|
|
4374
|
+
|
|
4375
|
+
```svelte
|
|
4376
|
+
<!-- src/routes/shop/order/[number]/+page.svelte -->
|
|
4377
|
+
<OrderStatus {number} {token} />
|
|
4378
|
+
```
|
|
4379
|
+
|
|
4380
|
+
### Theming
|
|
4381
|
+
|
|
4382
|
+
All styling sits on CSS variables — override in your page or app.css:
|
|
4383
|
+
|
|
4384
|
+
```css
|
|
4385
|
+
.aria-order-status {
|
|
4386
|
+
--shop-bg: #171717;
|
|
4387
|
+
--shop-fg: #f2e9e1;
|
|
4388
|
+
--shop-muted: rgba(242, 233, 225, 0.6);
|
|
4389
|
+
--shop-accent: #f2e9e1;
|
|
4390
|
+
--shop-success: #8fb58c;
|
|
4391
|
+
--shop-error: #c44b4b;
|
|
4392
|
+
--shop-border: rgba(242, 233, 225, 0.15);
|
|
4393
|
+
--shop-radius: 2px;
|
|
4394
|
+
--shop-font-display: 'Syncopate', sans-serif;
|
|
4395
|
+
--shop-font-body: 'Figtree', sans-serif;
|
|
4396
|
+
}
|
|
4397
|
+
```
|
|
4398
|
+
|
|
4399
|
+
Fine-grained overrides via `part="..."` selectors:
|
|
4400
|
+
|
|
4401
|
+
```css
|
|
4402
|
+
.aria-order-status::part(title) { text-transform: uppercase; }
|
|
4403
|
+
.aria-order-status::part(btn-retry) { letter-spacing: 0.08em; }
|
|
4404
|
+
```
|
|
4405
|
+
|
|
4406
|
+
### Props
|
|
4407
|
+
|
|
4408
|
+
| Prop | Type | Notes |
|
|
4409
|
+
|------|------|-------|
|
|
4410
|
+
| `number` | `string` | Order number (from URL) |
|
|
4411
|
+
| `token` | `string?` | Access token from `?token=`; falls back to cookie |
|
|
4412
|
+
| `initialData` | `OrderDetailResponse?` | SSR snapshot from `+page.server.ts` |
|
|
4413
|
+
| `labels` | `Partial<OrderStatusLabels>` | Override copy (Polish by default) |
|
|
4414
|
+
| `autoPoll` | `boolean` | Default `true` — polls every 5s when status is `awaitingPayment` |
|
|
4415
|
+
| `onPaid` | `(data) => void` | Fires the first time status becomes `paid` |
|
|
4416
|
+
|
|
4417
|
+
### SSR
|
|
4418
|
+
|
|
4419
|
+
Fetch on the server for instant render and email-from-SPA support:
|
|
4420
|
+
|
|
4421
|
+
```ts
|
|
4422
|
+
// +page.server.ts
|
|
4423
|
+
import { createShopClient } from 'includio-cms/shop/client';
|
|
4424
|
+
import { readOrderTokenCookie } from 'includio-cms/shop/client'; // exposed from shop module
|
|
4425
|
+
|
|
4426
|
+
export const load = async ({ params, url, cookies, fetch }) => {
|
|
4427
|
+
const client = createShopClient({ fetch });
|
|
4428
|
+
const token = url.searchParams.get('token') ?? undefined;
|
|
4429
|
+
try {
|
|
4430
|
+
const initialData = await client.orders.get(params.number, token);
|
|
4431
|
+
return { initialData, token };
|
|
4432
|
+
} catch {
|
|
4433
|
+
return { initialData: null, token };
|
|
4434
|
+
}
|
|
4435
|
+
};
|
|
4436
|
+
```
|
|
4437
|
+
|
|
4438
|
+
## 2. Custom UI (headless)
|
|
4439
|
+
|
|
4440
|
+
For fully custom designs, use `createOrderState` — a reactive Svelte 5 state object. Build whatever markup you want.
|
|
4441
|
+
|
|
4442
|
+
```svelte
|
|
4443
|
+
{#if order.loading && !order.data}
|
|
4444
|
+
<p>Ładowanie…</p>
|
|
4445
|
+
{:else if order.data}
|
|
4446
|
+
{@const o = order.data.order}
|
|
4447
|
+
<h1>Zamówienie {o.number}</h1>
|
|
4448
|
+
<p>Status: {o.status}</p>
|
|
4449
|
+
|
|
4450
|
+
{#if o.status === 'awaitingPayment' || o.status === 'paymentRejected'}
|
|
4451
|
+
<button onclick={handleRetry} disabled={order.phase === 'retrying'}>
|
|
4452
|
+
Zapłać ponownie
|
|
4453
|
+
</button>
|
|
4454
|
+
{/if}
|
|
4455
|
+
{/if}
|
|
4456
|
+
```
|
|
4457
|
+
|
|
4458
|
+
### State API
|
|
4459
|
+
|
|
4460
|
+
- `order.data` — `OrderDetailResponse | null`
|
|
4461
|
+
- `order.loading` — initial fetch flag
|
|
4462
|
+
- `order.error` — last error (`Error | null`)
|
|
4463
|
+
- `order.phase` — `'idle' | 'polling' | 'retrying' | 'refreshing'`
|
|
4464
|
+
- `order.status` — shorthand for `data.order.status`
|
|
4465
|
+
- `order.load()` — fetch fresh data
|
|
4466
|
+
- `order.refreshPayment()` — ask provider for latest (useful if webhook is lost)
|
|
4467
|
+
- `order.retry()` — create new payment attempt, returns `redirectUrl | null`
|
|
4468
|
+
- `order.startPolling() / stopPolling()` — auto-refresh every 5s (max 2min), stops on terminal status
|
|
4469
|
+
- `order.dispose()` — clear timers (call in `onDestroy`)
|
|
4470
|
+
|
|
4471
|
+
## Security
|
|
4472
|
+
|
|
4473
|
+
- Every order has an `accessToken` (uuid). The view API (`/api/shop/orders/[number]`) requires it in `?token=` or in the `aria_shop_order` cookie (same-browser fallback, 30-min httpOnly).
|
|
4474
|
+
- Status emails include the full URL with token — the customer can return from any device.
|
|
4475
|
+
- Order numbers are Crockford base32 but token-gating prevents enumeration regardless.
|
|
4476
|
+
|
|
4477
|
+
|
|
4478
|
+
---
|
|
4479
|
+
|
|
4480
|
+
# Retry payment
|
|
4481
|
+
|
|
4482
|
+
When a payment attempt is rejected or left hanging, the customer must be able to try again without going through checkout a second time.
|
|
4483
|
+
|
|
4484
|
+
## Lifecycle
|
|
4485
|
+
|
|
4486
|
+
1. Customer completes checkout → order created with status `awaitingPayment`, adapter creates a payment session, `paymentProviderRef` stored.
|
|
4487
|
+
2. Something goes wrong — customer cancels on gateway, card declined, session expires → webhook flips status to `paymentRejected` (or stays `awaitingPayment` if the gateway never called back).
|
|
4488
|
+
3. Customer hits "Zapłać ponownie" on the order view.
|
|
4489
|
+
4. `POST /api/shop/orders/[number]/retry-payment?token=...` →
|
|
4490
|
+
- token-gated, status must be `awaitingPayment` or `paymentRejected` (409 otherwise)
|
|
4491
|
+
- adapter's `createPayment()` is called again with the same `OrderRef`
|
|
4492
|
+
- new `paymentProviderRef` replaces the old one
|
|
4493
|
+
- if coming from `paymentRejected`, status rolls back to `awaitingPayment` (logged in status history as `changedBy: 'retry-payment'`)
|
|
4494
|
+
- response contains `redirectUrl` for the new gateway session
|
|
4495
|
+
|
|
4496
|
+
## SDK
|
|
4497
|
+
|
|
4498
|
+
```ts
|
|
4499
|
+
import { createShopClient } from 'includio-cms/shop/client';
|
|
4500
|
+
|
|
4501
|
+
const client = createShopClient();
|
|
4502
|
+
const result = await client.orders.retryPayment(orderNumber, token);
|
|
4503
|
+
|
|
4504
|
+
if (result.requiresPaymentRedirect && result.redirectUrl) {
|
|
4505
|
+
window.location.href = result.redirectUrl;
|
|
4506
|
+
}
|
|
4507
|
+
```
|
|
4508
|
+
|
|
4509
|
+
## With `createOrderState`
|
|
4510
|
+
|
|
4511
|
+
The headless helper handles the redirect for you — it returns the URL, you navigate:
|
|
4512
|
+
|
|
4513
|
+
```ts
|
|
4514
|
+
const url = await order.retry();
|
|
4515
|
+
if (url) window.location.href = url;
|
|
4516
|
+
```
|
|
4517
|
+
|
|
4518
|
+
## Constraints
|
|
4519
|
+
|
|
4520
|
+
- The same order is reused — numbers, tokens, items, totals stay the same.
|
|
4521
|
+
- Terminal statuses (`paid`, `done`, `cancelled`) block retry with **409**.
|
|
4522
|
+
- Orders without a payment method or whose adapter was removed from config return **400**.
|
|
4523
|
+
- Adapter errors bubble up as **502**.
|
|
4524
|
+
|
|
4525
|
+
## Admin override
|
|
4526
|
+
|
|
4527
|
+
If the customer is completely stuck, the admin can manually move the order to `paid` / `cancelled` from the orders detail page — same as before, status-change emails trigger automatically.
|
|
4528
|
+
|
|
4529
|
+
|
|
4300
4530
|
---
|
|
4301
4531
|
|
|
4302
4532
|
# Migration Guide
|
package/ROADMAP.md
CHANGED
|
@@ -303,8 +303,8 @@
|
|
|
303
303
|
- [x] `[feature]` `[P0]` Admin orders view — list (status + email filter), detail (items, history, customer/address/consents, change status, resend email)
|
|
304
304
|
- [x] `[feature]` `[P0]` Random Crockford base32 order numbers (`XXXXX-XXXXX`), unguessable
|
|
305
305
|
- [x] `[feature]` `[P0]` CLI scaffold provisions all shop routes (admin + public API) in consumer app
|
|
306
|
-
- [
|
|
307
|
-
- [ ] `[feature]` `[P1]`
|
|
306
|
+
- [x] `[feature]` `[P1]` PayU payment adapter + webhook — shipped in 0.15.1 (access-token-gated order view, idempotent webhooks, MD5 signature, shipping↔payment compat, poll fallback)
|
|
307
|
+
- [ ] `[feature]` `[P1]` Stripe payment adapter + webhook — deferred to 0.15.2
|
|
308
308
|
- [ ] `[feature]` `[P2]` InPost carrier adapter (headless geowidget config) — deferred to 0.15.3
|
|
309
309
|
- [x] `[feature]` `[P2]` `nodemailerAdapter` — typowanie `transportOptions` jako `SMTPTransport.Options` (OAuth2, pool, itd.)
|
|
310
310
|
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
import { Switch } from '../../../components/ui/switch/index.js';
|
|
4
4
|
|
|
5
5
|
type CarrierType = 'none' | 'inpost';
|
|
6
|
+
export interface PaymentMethodOption {
|
|
7
|
+
id: string;
|
|
8
|
+
label: Record<string, string>;
|
|
9
|
+
}
|
|
6
10
|
export interface ShippingFormPayload {
|
|
7
11
|
name: Record<string, string>;
|
|
8
12
|
description: Record<string, string> | null;
|
|
@@ -10,6 +14,7 @@
|
|
|
10
14
|
vatRate: number;
|
|
11
15
|
carrierType: CarrierType;
|
|
12
16
|
conditions: { freeAbove?: number } | null;
|
|
17
|
+
allowedPaymentMethods: string[] | null;
|
|
13
18
|
isActive: boolean;
|
|
14
19
|
sortOrder: number | null;
|
|
15
20
|
}
|
|
@@ -20,6 +25,7 @@
|
|
|
20
25
|
vatRate?: number;
|
|
21
26
|
carrierType?: string;
|
|
22
27
|
conditions?: { freeAbove?: number } | null;
|
|
28
|
+
allowedPaymentMethods?: string[] | null;
|
|
23
29
|
isActive?: boolean;
|
|
24
30
|
sortOrder?: number | null;
|
|
25
31
|
}
|
|
@@ -27,6 +33,7 @@
|
|
|
27
33
|
interface Props {
|
|
28
34
|
languages: string[];
|
|
29
35
|
vatRates: number[];
|
|
36
|
+
paymentMethods?: PaymentMethodOption[];
|
|
30
37
|
initial?: ShippingFormInitial | null;
|
|
31
38
|
saving?: boolean;
|
|
32
39
|
errorMessage?: string | null;
|
|
@@ -37,6 +44,7 @@
|
|
|
37
44
|
const {
|
|
38
45
|
languages,
|
|
39
46
|
vatRates,
|
|
47
|
+
paymentMethods = [],
|
|
40
48
|
initial = null,
|
|
41
49
|
saving = false,
|
|
42
50
|
errorMessage = null,
|
|
@@ -87,6 +95,26 @@
|
|
|
87
95
|
}
|
|
88
96
|
let carrierType = $state<CarrierType>((initial?.carrierType as CarrierType) ?? 'none');
|
|
89
97
|
let isActive = $state(initial?.isActive ?? true);
|
|
98
|
+
// null/undefined = no restriction (all allowed); otherwise a whitelist.
|
|
99
|
+
let restrictPayments = $state(
|
|
100
|
+
Array.isArray(initial?.allowedPaymentMethods) && (initial?.allowedPaymentMethods?.length ?? 0) > 0
|
|
101
|
+
);
|
|
102
|
+
let allowedPaymentIds = $state<string[]>(
|
|
103
|
+
Array.isArray(initial?.allowedPaymentMethods) ? [...(initial?.allowedPaymentMethods ?? [])] : []
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
function togglePaymentMethod(id: string, checked: boolean) {
|
|
107
|
+
if (checked) {
|
|
108
|
+
if (!allowedPaymentIds.includes(id)) allowedPaymentIds = [...allowedPaymentIds, id];
|
|
109
|
+
} else {
|
|
110
|
+
allowedPaymentIds = allowedPaymentIds.filter((x) => x !== id);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function paymentLabel(p: PaymentMethodOption): string {
|
|
115
|
+
const first = languages[0] ?? 'pl';
|
|
116
|
+
return p.label[first] ?? Object.values(p.label)[0] ?? p.id;
|
|
117
|
+
}
|
|
90
118
|
let freeAboveEnabled = $state(initial?.conditions?.freeAbove != null);
|
|
91
119
|
let freeAbove = $state(
|
|
92
120
|
initial?.conditions?.freeAbove != null
|
|
@@ -104,6 +132,7 @@
|
|
|
104
132
|
conditions: freeAboveEnabled
|
|
105
133
|
? { freeAbove: Math.round(parseFloat(freeAbove || '0') * 100) }
|
|
106
134
|
: null,
|
|
135
|
+
allowedPaymentMethods: restrictPayments ? allowedPaymentIds : null,
|
|
107
136
|
isActive,
|
|
108
137
|
sortOrder: initial?.sortOrder ?? null
|
|
109
138
|
});
|
|
@@ -226,6 +255,42 @@
|
|
|
226
255
|
{/if}
|
|
227
256
|
</section>
|
|
228
257
|
|
|
258
|
+
{#if paymentMethods.length > 0}
|
|
259
|
+
<section class="border-border bg-card space-y-4 rounded-xl border p-6">
|
|
260
|
+
<h2 class="text-lg font-bold">Dozwolone metody płatności</h2>
|
|
261
|
+
<p class="text-muted-foreground text-sm">
|
|
262
|
+
Ogranicz metody płatności dla tej dostawy (np. wyłącz płatność za pobraniem dla
|
|
263
|
+
paczkomatu). Jeśli wyłączone — wszystkie skonfigurowane metody są dostępne.
|
|
264
|
+
</p>
|
|
265
|
+
<label class="flex items-center gap-2">
|
|
266
|
+
<Switch bind:checked={restrictPayments} />
|
|
267
|
+
<span class="text-sm">Ogranicz metody płatności</span>
|
|
268
|
+
</label>
|
|
269
|
+
{#if restrictPayments}
|
|
270
|
+
<div class="space-y-2 pl-2">
|
|
271
|
+
{#each paymentMethods as p (p.id)}
|
|
272
|
+
<label class="flex items-center gap-2">
|
|
273
|
+
<input
|
|
274
|
+
type="checkbox"
|
|
275
|
+
checked={allowedPaymentIds.includes(p.id)}
|
|
276
|
+
onchange={(e) =>
|
|
277
|
+
togglePaymentMethod(p.id, (e.currentTarget as HTMLInputElement).checked)}
|
|
278
|
+
/>
|
|
279
|
+
<span class="text-sm">{paymentLabel(p)}</span>
|
|
280
|
+
<span class="text-muted-foreground text-xs">({p.id})</span>
|
|
281
|
+
</label>
|
|
282
|
+
{/each}
|
|
283
|
+
{#if allowedPaymentIds.length === 0}
|
|
284
|
+
<p class="text-xs text-amber-700">
|
|
285
|
+
Wybierz co najmniej jedną metodę, inaczej checkout dla tej dostawy będzie
|
|
286
|
+
zablokowany.
|
|
287
|
+
</p>
|
|
288
|
+
{/if}
|
|
289
|
+
</div>
|
|
290
|
+
{/if}
|
|
291
|
+
</section>
|
|
292
|
+
{/if}
|
|
293
|
+
|
|
229
294
|
<section class="border-border bg-card space-y-4 rounded-xl border p-6">
|
|
230
295
|
<h2 class="text-lg font-bold">Przewoźnik</h2>
|
|
231
296
|
<label class="block">
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
type CarrierType = 'none' | 'inpost';
|
|
2
|
+
export interface PaymentMethodOption {
|
|
3
|
+
id: string;
|
|
4
|
+
label: Record<string, string>;
|
|
5
|
+
}
|
|
2
6
|
export interface ShippingFormPayload {
|
|
3
7
|
name: Record<string, string>;
|
|
4
8
|
description: Record<string, string> | null;
|
|
@@ -8,6 +12,7 @@ export interface ShippingFormPayload {
|
|
|
8
12
|
conditions: {
|
|
9
13
|
freeAbove?: number;
|
|
10
14
|
} | null;
|
|
15
|
+
allowedPaymentMethods: string[] | null;
|
|
11
16
|
isActive: boolean;
|
|
12
17
|
sortOrder: number | null;
|
|
13
18
|
}
|
|
@@ -20,12 +25,14 @@ export interface ShippingFormInitial {
|
|
|
20
25
|
conditions?: {
|
|
21
26
|
freeAbove?: number;
|
|
22
27
|
} | null;
|
|
28
|
+
allowedPaymentMethods?: string[] | null;
|
|
23
29
|
isActive?: boolean;
|
|
24
30
|
sortOrder?: number | null;
|
|
25
31
|
}
|
|
26
32
|
interface Props {
|
|
27
33
|
languages: string[];
|
|
28
34
|
vatRates: number[];
|
|
35
|
+
paymentMethods?: PaymentMethodOption[];
|
|
29
36
|
initial?: ShippingFormInitial | null;
|
|
30
37
|
saving?: boolean;
|
|
31
38
|
errorMessage?: string | null;
|
|
@@ -4,6 +4,10 @@ export declare const getShopConfig: import("@sveltejs/kit").RemoteQueryFunction<
|
|
|
4
4
|
vatRates: number[];
|
|
5
5
|
features: Required<import("../../shop/types.js").ShopFeatures>;
|
|
6
6
|
languages: import("../../types/languages.js").Language[];
|
|
7
|
+
paymentMethods: {
|
|
8
|
+
id: string;
|
|
9
|
+
label: import("../../shop/types.js").I18nText;
|
|
10
|
+
}[];
|
|
7
11
|
} | null>;
|
|
8
12
|
export declare const listShopProductEntries: import("@sveltejs/kit").RemoteQueryFunction<void, import("../../shop/server/shop-data.js").ShopEntryListItem[]>;
|
|
9
13
|
export declare const getShopDataForEntry: import("@sveltejs/kit").RemoteQueryFunction<string, import("../../shop/server/shop-data.js").ShopDataWithVariants | null>;
|
|
@@ -40,6 +44,7 @@ export declare const listShippingMethodsAdmin: import("@sveltejs/kit").RemoteQue
|
|
|
40
44
|
conditions: {
|
|
41
45
|
freeAbove?: number;
|
|
42
46
|
} | null;
|
|
47
|
+
allowedPaymentMethods: string[] | null;
|
|
43
48
|
}[]>;
|
|
44
49
|
export declare const getShippingMethodForAdmin: import("@sveltejs/kit").RemoteQueryFunction<string, {
|
|
45
50
|
id: string;
|
|
@@ -54,6 +59,7 @@ export declare const getShippingMethodForAdmin: import("@sveltejs/kit").RemoteQu
|
|
|
54
59
|
conditions: {
|
|
55
60
|
freeAbove?: number;
|
|
56
61
|
} | null;
|
|
62
|
+
allowedPaymentMethods: string[] | null;
|
|
57
63
|
} | null>;
|
|
58
64
|
export declare const createShippingMethodCmd: import("@sveltejs/kit").RemoteCommand<{
|
|
59
65
|
name: Record<string, string>;
|
|
@@ -64,6 +70,7 @@ export declare const createShippingMethodCmd: import("@sveltejs/kit").RemoteComm
|
|
|
64
70
|
conditions?: {
|
|
65
71
|
freeAbove?: number | undefined;
|
|
66
72
|
} | null | undefined;
|
|
73
|
+
allowedPaymentMethods?: string[] | null | undefined;
|
|
67
74
|
isActive?: boolean | undefined;
|
|
68
75
|
sortOrder?: number | null | undefined;
|
|
69
76
|
}, Promise<{
|
|
@@ -79,6 +86,7 @@ export declare const createShippingMethodCmd: import("@sveltejs/kit").RemoteComm
|
|
|
79
86
|
conditions: {
|
|
80
87
|
freeAbove?: number;
|
|
81
88
|
} | null;
|
|
89
|
+
allowedPaymentMethods: string[] | null;
|
|
82
90
|
}>>;
|
|
83
91
|
export declare const updateShippingMethodCmd: import("@sveltejs/kit").RemoteCommand<{
|
|
84
92
|
id: string;
|
|
@@ -91,6 +99,7 @@ export declare const updateShippingMethodCmd: import("@sveltejs/kit").RemoteComm
|
|
|
91
99
|
conditions?: {
|
|
92
100
|
freeAbove?: number | undefined;
|
|
93
101
|
} | null | undefined;
|
|
102
|
+
allowedPaymentMethods?: string[] | null | undefined;
|
|
94
103
|
isActive?: boolean | undefined;
|
|
95
104
|
sortOrder?: number | null | undefined;
|
|
96
105
|
};
|
|
@@ -107,6 +116,7 @@ export declare const updateShippingMethodCmd: import("@sveltejs/kit").RemoteComm
|
|
|
107
116
|
conditions: {
|
|
108
117
|
freeAbove?: number;
|
|
109
118
|
} | null;
|
|
119
|
+
allowedPaymentMethods: string[] | null;
|
|
110
120
|
}>>;
|
|
111
121
|
export declare const deleteShippingMethodCmd: import("@sveltejs/kit").RemoteCommand<string, Promise<{
|
|
112
122
|
success: boolean;
|
|
@@ -126,6 +136,7 @@ export declare const listOrdersAdmin: import("@sveltejs/kit").RemoteQueryFunctio
|
|
|
126
136
|
createdAt: Date;
|
|
127
137
|
updatedAt: Date;
|
|
128
138
|
language: string | null;
|
|
139
|
+
accessToken: string;
|
|
129
140
|
consents: {
|
|
130
141
|
id: string;
|
|
131
142
|
accepted: boolean;
|
|
@@ -145,6 +156,7 @@ export declare const listOrdersAdmin: import("@sveltejs/kit").RemoteQueryFunctio
|
|
|
145
156
|
shippingMethodId: string | null;
|
|
146
157
|
carrierRef: string | null;
|
|
147
158
|
paymentMethod: string | null;
|
|
159
|
+
paymentProviderRef: string | null;
|
|
148
160
|
notes: string | null;
|
|
149
161
|
}[]>;
|
|
150
162
|
export declare const getOrderForAdmin: import("@sveltejs/kit").RemoteQueryFunction<string, {
|
|
@@ -155,6 +167,7 @@ export declare const getOrderForAdmin: import("@sveltejs/kit").RemoteQueryFuncti
|
|
|
155
167
|
createdAt: Date;
|
|
156
168
|
updatedAt: Date;
|
|
157
169
|
language: string | null;
|
|
170
|
+
accessToken: string;
|
|
158
171
|
consents: {
|
|
159
172
|
id: string;
|
|
160
173
|
accepted: boolean;
|
|
@@ -174,6 +187,7 @@ export declare const getOrderForAdmin: import("@sveltejs/kit").RemoteQueryFuncti
|
|
|
174
187
|
shippingMethodId: string | null;
|
|
175
188
|
carrierRef: string | null;
|
|
176
189
|
paymentMethod: string | null;
|
|
190
|
+
paymentProviderRef: string | null;
|
|
177
191
|
notes: string | null;
|
|
178
192
|
};
|
|
179
193
|
items: {
|
|
@@ -208,6 +222,7 @@ export declare const updateOrderStatusCmd: import("@sveltejs/kit").RemoteCommand
|
|
|
208
222
|
createdAt: Date;
|
|
209
223
|
updatedAt: Date;
|
|
210
224
|
language: string | null;
|
|
225
|
+
accessToken: string;
|
|
211
226
|
consents: {
|
|
212
227
|
id: string;
|
|
213
228
|
accepted: boolean;
|
|
@@ -227,6 +242,7 @@ export declare const updateOrderStatusCmd: import("@sveltejs/kit").RemoteCommand
|
|
|
227
242
|
shippingMethodId: string | null;
|
|
228
243
|
carrierRef: string | null;
|
|
229
244
|
paymentMethod: string | null;
|
|
245
|
+
paymentProviderRef: string | null;
|
|
230
246
|
notes: string | null;
|
|
231
247
|
}>>;
|
|
232
248
|
export declare const resendOrderEmailCmd: import("@sveltejs/kit").RemoteCommand<{
|
|
@@ -18,7 +18,8 @@ export const getShopConfig = query(async () => {
|
|
|
18
18
|
currency: shop.currency,
|
|
19
19
|
vatRates: shop.vatRates,
|
|
20
20
|
features: shop.features,
|
|
21
|
-
languages: getCMS().languages
|
|
21
|
+
languages: getCMS().languages,
|
|
22
|
+
paymentMethods: shop.payment.map((p) => ({ id: p.id, label: p.label }))
|
|
22
23
|
};
|
|
23
24
|
});
|
|
24
25
|
export const listShopProductEntries = query(async () => {
|
|
@@ -66,6 +67,7 @@ const shippingMethodInputSchema = z.object({
|
|
|
66
67
|
.object({ freeAbove: z.number().int().nonnegative().optional() })
|
|
67
68
|
.nullable()
|
|
68
69
|
.optional(),
|
|
70
|
+
allowedPaymentMethods: z.array(z.string()).nullable().optional(),
|
|
69
71
|
isActive: z.boolean().optional(),
|
|
70
72
|
sortOrder: z.number().int().nullable().optional()
|
|
71
73
|
});
|
|
@@ -214,6 +214,38 @@ export const { GET } = createShippingMethodsHandler();
|
|
|
214
214
|
import { createCheckoutHandler } from 'includio-cms/shop/http';
|
|
215
215
|
|
|
216
216
|
export const { POST } = createCheckoutHandler();
|
|
217
|
+
`
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
path: 'api/shop/orders/[number]/+server.ts',
|
|
221
|
+
content: `${GENERATED_COMMENT_TS}
|
|
222
|
+
import { createOrderHandler } from 'includio-cms/shop/http';
|
|
223
|
+
|
|
224
|
+
export const { GET } = createOrderHandler();
|
|
225
|
+
`
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
path: 'api/shop/webhooks/[provider]/+server.ts',
|
|
229
|
+
content: `${GENERATED_COMMENT_TS}
|
|
230
|
+
import { createPaymentWebhookHandler } from 'includio-cms/shop/http';
|
|
231
|
+
|
|
232
|
+
export const { POST } = createPaymentWebhookHandler();
|
|
233
|
+
`
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
path: 'api/shop/orders/[number]/refresh-payment/+server.ts',
|
|
237
|
+
content: `${GENERATED_COMMENT_TS}
|
|
238
|
+
import { createRefreshPaymentHandler } from 'includio-cms/shop/http';
|
|
239
|
+
|
|
240
|
+
export const { POST } = createRefreshPaymentHandler();
|
|
241
|
+
`
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
path: 'api/shop/orders/[number]/retry-payment/+server.ts',
|
|
245
|
+
content: `${GENERATED_COMMENT_TS}
|
|
246
|
+
import { createRetryPaymentHandler } from 'includio-cms/shop/http';
|
|
247
|
+
|
|
248
|
+
export const { POST } = createRetryPaymentHandler();
|
|
217
249
|
`
|
|
218
250
|
},
|
|
219
251
|
{
|
|
@@ -296,6 +296,23 @@ export declare const shopOrdersTable: import("drizzle-orm/pg-core/table", { with
|
|
|
296
296
|
identity: undefined;
|
|
297
297
|
generated: undefined;
|
|
298
298
|
}, {}, {}>;
|
|
299
|
+
paymentProviderRef: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
300
|
+
name: "payment_provider_ref";
|
|
301
|
+
tableName: "shop_orders";
|
|
302
|
+
dataType: "string";
|
|
303
|
+
columnType: "PgText";
|
|
304
|
+
data: string;
|
|
305
|
+
driverParam: string;
|
|
306
|
+
notNull: false;
|
|
307
|
+
hasDefault: false;
|
|
308
|
+
isPrimaryKey: false;
|
|
309
|
+
isAutoincrement: false;
|
|
310
|
+
hasRuntimeDefault: false;
|
|
311
|
+
enumValues: [string, ...string[]];
|
|
312
|
+
baseColumn: never;
|
|
313
|
+
identity: undefined;
|
|
314
|
+
generated: undefined;
|
|
315
|
+
}, {}, {}>;
|
|
299
316
|
consents: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
300
317
|
name: "consents";
|
|
301
318
|
tableName: "shop_orders";
|
|
@@ -357,6 +374,23 @@ export declare const shopOrdersTable: import("drizzle-orm/pg-core/table", { with
|
|
|
357
374
|
identity: undefined;
|
|
358
375
|
generated: undefined;
|
|
359
376
|
}, {}, {}>;
|
|
377
|
+
accessToken: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
378
|
+
name: "access_token";
|
|
379
|
+
tableName: "shop_orders";
|
|
380
|
+
dataType: "string";
|
|
381
|
+
columnType: "PgUUID";
|
|
382
|
+
data: string;
|
|
383
|
+
driverParam: string;
|
|
384
|
+
notNull: true;
|
|
385
|
+
hasDefault: true;
|
|
386
|
+
isPrimaryKey: false;
|
|
387
|
+
isAutoincrement: false;
|
|
388
|
+
hasRuntimeDefault: false;
|
|
389
|
+
enumValues: undefined;
|
|
390
|
+
baseColumn: never;
|
|
391
|
+
identity: undefined;
|
|
392
|
+
generated: undefined;
|
|
393
|
+
}, {}, {}>;
|
|
360
394
|
createdAt: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
361
395
|
name: "created_at";
|
|
362
396
|
tableName: "shop_orders";
|