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.
Files changed (80) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/DOCS.md +231 -1
  3. package/ROADMAP.md +2 -2
  4. package/dist/admin/client/shop/shipping-method-edit-page.svelte +1 -0
  5. package/dist/admin/client/shop/shipping-method-form.svelte +65 -0
  6. package/dist/admin/client/shop/shipping-method-form.svelte.d.ts +7 -0
  7. package/dist/admin/client/shop/shipping-method-new-page.svelte +1 -0
  8. package/dist/admin/remote/shop.remote.d.ts +16 -0
  9. package/dist/admin/remote/shop.remote.js +3 -1
  10. package/dist/cli/scaffold/admin.js +32 -0
  11. package/dist/db-postgres/schema/shop/order.d.ts +34 -0
  12. package/dist/db-postgres/schema/shop/order.js +2 -0
  13. package/dist/db-postgres/schema/shop/shippingMethod.d.ts +19 -0
  14. package/dist/db-postgres/schema/shop/shippingMethod.js +1 -0
  15. package/dist/paraglide/messages/_index.d.ts +3 -36
  16. package/dist/paraglide/messages/_index.js +3 -71
  17. package/dist/paraglide/messages/hello_world.d.ts +5 -0
  18. package/dist/paraglide/messages/hello_world.js +33 -0
  19. package/dist/paraglide/messages/login_hello.d.ts +16 -0
  20. package/dist/paraglide/messages/login_hello.js +34 -0
  21. package/dist/paraglide/messages/login_please_login.d.ts +16 -0
  22. package/dist/paraglide/messages/login_please_login.js +34 -0
  23. package/dist/shop/adapters/payu/client.d.ts +22 -0
  24. package/dist/shop/adapters/payu/client.js +78 -0
  25. package/dist/shop/adapters/payu/index.d.ts +24 -0
  26. package/dist/shop/adapters/payu/index.js +88 -0
  27. package/dist/shop/adapters/payu/payload.d.ts +48 -0
  28. package/dist/shop/adapters/payu/payload.js +48 -0
  29. package/dist/shop/adapters/payu/signature.d.ts +12 -0
  30. package/dist/shop/adapters/payu/signature.js +50 -0
  31. package/dist/shop/adapters/payu/status-map.d.ts +3 -0
  32. package/dist/shop/adapters/payu/status-map.js +14 -0
  33. package/dist/shop/cart/order-token-cookie.d.ts +9 -0
  34. package/dist/shop/cart/order-token-cookie.js +40 -0
  35. package/dist/shop/client/index.d.ts +63 -1
  36. package/dist/shop/client/index.js +9 -0
  37. package/dist/shop/client/use-order.svelte.d.ts +32 -0
  38. package/dist/shop/client/use-order.svelte.js +105 -0
  39. package/dist/shop/http/checkout-handler.js +47 -4
  40. package/dist/shop/http/index.d.ts +4 -0
  41. package/dist/shop/http/index.js +4 -0
  42. package/dist/shop/http/order-handler.d.ts +4 -0
  43. package/dist/shop/http/order-handler.js +85 -0
  44. package/dist/shop/http/refresh-payment-handler.d.ts +4 -0
  45. package/dist/shop/http/refresh-payment-handler.js +73 -0
  46. package/dist/shop/http/retry-payment-handler.d.ts +4 -0
  47. package/dist/shop/http/retry-payment-handler.js +99 -0
  48. package/dist/shop/http/retry-payment-logic.d.ts +2 -0
  49. package/dist/shop/http/retry-payment-logic.js +4 -0
  50. package/dist/shop/http/shipping-handler.js +2 -1
  51. package/dist/shop/http/webhook-handler.d.ts +4 -0
  52. package/dist/shop/http/webhook-handler.js +73 -0
  53. package/dist/shop/http/webhook-logic.d.ts +4 -0
  54. package/dist/shop/http/webhook-logic.js +21 -0
  55. package/dist/shop/index.d.ts +3 -1
  56. package/dist/shop/index.js +3 -1
  57. package/dist/shop/server/email.js +18 -2
  58. package/dist/shop/server/order-access-url.d.ts +7 -0
  59. package/dist/shop/server/order-access-url.js +6 -0
  60. package/dist/shop/server/orders.d.ts +1 -0
  61. package/dist/shop/server/orders.js +12 -0
  62. package/dist/shop/server/payment-compat.d.ts +5 -0
  63. package/dist/shop/server/payment-compat.js +9 -0
  64. package/dist/shop/server/shipping.d.ts +1 -0
  65. package/dist/shop/server/shipping.js +3 -0
  66. package/dist/shop/svelte/OrderStatus.svelte +368 -0
  67. package/dist/shop/svelte/OrderStatus.svelte.d.ts +14 -0
  68. package/dist/shop/svelte/index.d.ts +3 -0
  69. package/dist/shop/svelte/index.js +2 -0
  70. package/dist/shop/svelte/labels.d.ts +25 -0
  71. package/dist/shop/svelte/labels.js +41 -0
  72. package/dist/shop/types.d.ts +19 -1
  73. package/dist/updates/0.15.1/index.d.ts +2 -0
  74. package/dist/updates/0.15.1/index.js +27 -0
  75. package/dist/updates/index.js +2 -1
  76. package/package.json +5 -1
  77. package/dist/paraglide/messages/en.d.ts +0 -5
  78. package/dist/paraglide/messages/en.js +0 -14
  79. package/dist/paraglide/messages/pl.d.ts +0 -5
  80. 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.0)
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
- - [ ] `[feature]` `[P1]` Stripe payment adapter + webhook — deferred to 0.15.1
307
- - [ ] `[feature]` `[P1]` PayU payment adapter + webhook — deferred to 0.15.2
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
 
@@ -104,6 +104,7 @@
104
104
  <ShippingMethodForm
105
105
  languages={configQuery.current.languages}
106
106
  vatRates={configQuery.current.vatRates}
107
+ paymentMethods={configQuery.current.paymentMethods}
107
108
  initial={methodQuery.current}
108
109
  {saving}
109
110
  {errorMessage}
@@ -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;
@@ -39,6 +39,7 @@
39
39
  <ShippingMethodForm
40
40
  languages={configQuery.current.languages}
41
41
  vatRates={configQuery.current.vatRates}
42
+ paymentMethods={configQuery.current.paymentMethods}
42
43
  {saving}
43
44
  {errorMessage}
44
45
  onsubmit={handleSubmit}
@@ -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";