includio-cms 0.15.0 → 0.15.2
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 +61 -0
- package/DOCS.md +231 -1
- package/ROADMAP.md +7 -2
- package/dist/admin/client/shop/shipping-method-edit-page.svelte +1 -0
- package/dist/admin/client/shop/shipping-method-form.svelte +89 -21
- package/dist/admin/client/shop/shipping-method-form.svelte.d.ts +8 -1
- package/dist/admin/client/shop/shipping-method-new-page.svelte +1 -0
- package/dist/admin/client/shop/shipping-methods-list-page.svelte +7 -4
- package/dist/admin/client/shop/shop-products-list-page.svelte +2 -2
- package/dist/admin/components/fields/shop-field.svelte +63 -22
- package/dist/admin/remote/shop.remote.d.ts +16 -56
- package/dist/admin/remote/shop.remote.js +6 -4
- 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/product.d.ts +4 -4
- package/dist/db-postgres/schema/shop/product.js +3 -2
- package/dist/db-postgres/schema/shop/productVariant.d.ts +4 -4
- package/dist/db-postgres/schema/shop/productVariant.js +3 -2
- package/dist/db-postgres/schema/shop/shippingMethod.d.ts +23 -4
- package/dist/db-postgres/schema/shop/shippingMethod.js +4 -2
- 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 +64 -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/pricing.d.ts +4 -0
- package/dist/shop/pricing.js +18 -0
- package/dist/shop/server/cart-hydrate.js +6 -3
- 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/populate.d.ts +2 -0
- package/dist/shop/server/shipping.d.ts +12 -4
- package/dist/shop/server/shipping.js +24 -14
- package/dist/shop/server/shop-data.d.ts +8 -2
- package/dist/shop/server/shop-data.js +18 -10
- 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/0.15.2/index.d.ts +2 -0
- package/dist/updates/0.15.2/index.js +18 -0
- package/dist/updates/index.js +3 -1
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,67 @@
|
|
|
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.2 — 2026-04-15
|
|
7
|
+
|
|
8
|
+
Shop: cena przechowywana jako numeric(20,6) — eliminacja driftu brutto/netto po reload; toggle netto/brutto per wariant.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Warianty produktu mają teraz toggle netto/brutto obok pola "Zmiana ceny" — spójne z toggle'em ceny bazowej. Delta zapisywana kanonicznie jako netto.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- Shop: po wpisaniu ceny brutto (np. 65,00 zł przy VAT 23%) i refreshu, cena nie "ucieka" o ±1 grosz. Wartości przechowywane są jako PLN z 6 miejscami po przecinku (wzorzec PrestaShop), zamiast jednostek groszy — brutto zawsze odtwarzane z netto bez utraty informacji.
|
|
15
|
+
- Shipping: ta sama poprawka dla ceny metody wysyłki (stored jako netto PLN z 6dp).
|
|
16
|
+
|
|
17
|
+
### Breaking
|
|
18
|
+
- Kolumny `shop_products.base_price`, `shop_product_variants.price_delta`, `shop_shipping_methods.price` zmieniły typ z `integer` (grosze) na `numeric(20,6)` (PLN). Snapshot zamówienia (`shop_orders.*`, `shop_order_items.price_*_snapshot`) pozostaje w groszach (`integer`) — KSeF-compatible.
|
|
19
|
+
- Publiczne typy `ShopDataWithVariants.basePrice`, `VariantRow.priceDelta`, `ShippingMethodRow.price` — wartość dalej `number`, ale wyrażona w PLN (nie groszach). Dla konsumentów SDK/populate: mnożenie × 100 jeśli potrzebujesz groszy.
|
|
20
|
+
|
|
21
|
+
### Migration
|
|
22
|
+
|
|
23
|
+
```sql
|
|
24
|
+
ALTER TABLE shop_products ALTER COLUMN base_price TYPE numeric(20,6) USING (base_price::numeric / 100);
|
|
25
|
+
ALTER TABLE shop_product_variants ALTER COLUMN price_delta TYPE numeric(20,6) USING (price_delta::numeric / 100);
|
|
26
|
+
ALTER TABLE shop_shipping_methods ALTER COLUMN price TYPE numeric(20,6) USING (price::numeric / 100);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Notes
|
|
30
|
+
|
|
31
|
+
Migrację SQL trzeba uruchomić RAZ, PRZED `db:push` — dzieli istniejące wartości ÷100, bo stare dane były w groszach, a nowy typ przechowuje PLN. Po migracji `db:push` tylko zsynchronizuje schemat (bez zmiany danych).
|
|
32
|
+
|
|
33
|
+
## 0.15.1 — 2026-04-15
|
|
34
|
+
|
|
35
|
+
Shop: PayU payment adapter + secure order access (token-gated view API, email link).
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
- Orders now carry an `accessToken` — public order-view API is gated by this token (no enumeration by order number).
|
|
39
|
+
- 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).
|
|
40
|
+
- `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.
|
|
41
|
+
- 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.
|
|
42
|
+
- 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.
|
|
43
|
+
- Orders store `paymentProviderRef` — the external id returned by the payment adapter (e.g. PayU orderId). Used to correlate webhooks and future refunds/status polls.
|
|
44
|
+
- 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`.
|
|
45
|
+
- `PaymentAdapter.createPayment()` receives an optional `{ customerIp, language }` context, threaded from the SvelteKit request in the built-in checkout handler.
|
|
46
|
+
- 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.
|
|
47
|
+
- 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).
|
|
48
|
+
- 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.
|
|
49
|
+
- 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.
|
|
50
|
+
- 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`.
|
|
51
|
+
- 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`.
|
|
52
|
+
- 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`.
|
|
53
|
+
- Docs: new section covering the shop module, order view patterns (generic vs headless), and retry lifecycle.
|
|
54
|
+
|
|
55
|
+
### Migration
|
|
56
|
+
|
|
57
|
+
```sql
|
|
58
|
+
ALTER TABLE shop_orders ADD COLUMN access_token uuid NOT NULL DEFAULT gen_random_uuid();
|
|
59
|
+
ALTER TABLE shop_orders ADD COLUMN payment_provider_ref text;
|
|
60
|
+
ALTER TABLE shop_shipping_methods ADD COLUMN allowed_payment_methods jsonb;
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Notes
|
|
64
|
+
|
|
65
|
+
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.
|
|
66
|
+
|
|
6
67
|
## 0.15.0 — 2026-04-14
|
|
7
68
|
|
|
8
69
|
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.2)
|
|
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,9 +303,14 @@
|
|
|
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.3
|
|
308
308
|
- [ ] `[feature]` `[P2]` InPost carrier adapter (headless geowidget config) — deferred to 0.15.3
|
|
309
|
+
|
|
310
|
+
## 0.15.2 — Price precision fix
|
|
311
|
+
|
|
312
|
+
- [x] `[fix]` `[P1]` Price drift on reload — storage jako `numeric(20,6)` (PLN z 6dp), brutto round-tripuje bez utraty ±1gr. Snapshot zamówienia dalej w groszach (KSeF). <!-- files: src/lib/db-postgres/schema/shop/product.ts, productVariant.ts, shippingMethod.ts, src/lib/shop/pricing.ts, src/lib/shop/server/cart-hydrate.ts, shipping.ts, shop-data.ts, src/lib/admin/components/fields/shop-field.svelte, src/lib/admin/client/shop/shipping-method-form.svelte -->
|
|
313
|
+
- [x] `[feature]` `[P2]` Variant price toggle netto/brutto (spójne z toggle\'em ceny bazowej)
|
|
309
314
|
- [x] `[feature]` `[P2]` `nodemailerAdapter` — typowanie `transportOptions` jako `SMTPTransport.Options` (OAuth2, pool, itd.)
|
|
310
315
|
|
|
311
316
|
## 0.16.0 — SEO module
|
|
@@ -3,23 +3,29 @@
|
|
|
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;
|
|
9
|
-
price: number;
|
|
13
|
+
price: number; // PLN (number, netto)
|
|
10
14
|
vatRate: number;
|
|
11
15
|
carrierType: CarrierType;
|
|
12
|
-
conditions: { freeAbove?: number } | null;
|
|
16
|
+
conditions: { freeAbove?: number } | null; // freeAbove: grosze
|
|
17
|
+
allowedPaymentMethods: string[] | null;
|
|
13
18
|
isActive: boolean;
|
|
14
19
|
sortOrder: number | null;
|
|
15
20
|
}
|
|
16
21
|
export interface ShippingFormInitial {
|
|
17
22
|
name?: Record<string, string> | unknown;
|
|
18
23
|
description?: Record<string, string> | unknown | null;
|
|
19
|
-
price?: number;
|
|
24
|
+
price?: number | string;
|
|
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,
|
|
@@ -61,32 +69,55 @@
|
|
|
61
69
|
)
|
|
62
70
|
);
|
|
63
71
|
type InputMode = 'net' | 'gross';
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
// initial.price — teraz PLN (number, netto) z API. Hydratujemy jako gross, żeby user widział dokładnie to co wpisał.
|
|
73
|
+
const initialNetPln = initial?.price != null ? Number(initial.price) : null;
|
|
74
|
+
const initialVat = Number(initial?.vatRate ?? vatRates[0] ?? 23);
|
|
75
|
+
let inputMode = $state<InputMode>(initialNetPln != null ? 'gross' : 'gross');
|
|
76
|
+
let inputPrice = $state(
|
|
77
|
+
initialNetPln != null
|
|
78
|
+
? (initialNetPln * (1 + initialVat / 100)).toFixed(2)
|
|
79
|
+
: '0.00'
|
|
80
|
+
);
|
|
66
81
|
let vatRate = $state<number | string>(initial?.vatRate ?? vatRates[0] ?? 23);
|
|
67
82
|
|
|
68
|
-
const
|
|
83
|
+
const inputPln = $derived(parseFloat(inputPrice || '0') || 0);
|
|
69
84
|
const vat = $derived(Number(vatRate) || 0);
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
);
|
|
73
|
-
const grossCents = $derived(
|
|
74
|
-
inputMode === 'gross' ? inputPriceCents : Math.round(inputPriceCents * (1 + vat / 100))
|
|
75
|
-
);
|
|
76
|
-
const vatCents = $derived(grossCents - netCents);
|
|
85
|
+
const netPln = $derived(inputMode === 'net' ? inputPln : inputPln / (1 + vat / 100));
|
|
86
|
+
const grossPln = $derived(inputMode === 'gross' ? inputPln : inputPln * (1 + vat / 100));
|
|
87
|
+
const vatPln = $derived(grossPln - netPln);
|
|
77
88
|
|
|
78
|
-
function
|
|
79
|
-
return
|
|
89
|
+
function formatPln(pln: number) {
|
|
90
|
+
return pln.toFixed(2);
|
|
80
91
|
}
|
|
81
92
|
|
|
82
93
|
function switchMode(newMode: InputMode) {
|
|
83
94
|
if (newMode === inputMode) return;
|
|
84
|
-
const
|
|
95
|
+
const preserved = newMode === 'net' ? netPln : grossPln;
|
|
85
96
|
inputMode = newMode;
|
|
86
|
-
inputPrice =
|
|
97
|
+
inputPrice = formatPln(preserved);
|
|
87
98
|
}
|
|
88
99
|
let carrierType = $state<CarrierType>((initial?.carrierType as CarrierType) ?? 'none');
|
|
89
100
|
let isActive = $state(initial?.isActive ?? true);
|
|
101
|
+
// null/undefined = no restriction (all allowed); otherwise a whitelist.
|
|
102
|
+
let restrictPayments = $state(
|
|
103
|
+
Array.isArray(initial?.allowedPaymentMethods) && (initial?.allowedPaymentMethods?.length ?? 0) > 0
|
|
104
|
+
);
|
|
105
|
+
let allowedPaymentIds = $state<string[]>(
|
|
106
|
+
Array.isArray(initial?.allowedPaymentMethods) ? [...(initial?.allowedPaymentMethods ?? [])] : []
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
function togglePaymentMethod(id: string, checked: boolean) {
|
|
110
|
+
if (checked) {
|
|
111
|
+
if (!allowedPaymentIds.includes(id)) allowedPaymentIds = [...allowedPaymentIds, id];
|
|
112
|
+
} else {
|
|
113
|
+
allowedPaymentIds = allowedPaymentIds.filter((x) => x !== id);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function paymentLabel(p: PaymentMethodOption): string {
|
|
118
|
+
const first = languages[0] ?? 'pl';
|
|
119
|
+
return p.label[first] ?? Object.values(p.label)[0] ?? p.id;
|
|
120
|
+
}
|
|
90
121
|
let freeAboveEnabled = $state(initial?.conditions?.freeAbove != null);
|
|
91
122
|
let freeAbove = $state(
|
|
92
123
|
initial?.conditions?.freeAbove != null
|
|
@@ -98,12 +129,13 @@
|
|
|
98
129
|
await onsubmit({
|
|
99
130
|
name: names,
|
|
100
131
|
description: Object.values(descriptions).some((d) => d.length > 0) ? descriptions : null,
|
|
101
|
-
price:
|
|
132
|
+
price: netPln,
|
|
102
133
|
vatRate: Number(vatRate),
|
|
103
134
|
carrierType,
|
|
104
135
|
conditions: freeAboveEnabled
|
|
105
136
|
? { freeAbove: Math.round(parseFloat(freeAbove || '0') * 100) }
|
|
106
137
|
: null,
|
|
138
|
+
allowedPaymentMethods: restrictPayments ? allowedPaymentIds : null,
|
|
107
139
|
isActive,
|
|
108
140
|
sortOrder: initial?.sortOrder ?? null
|
|
109
141
|
});
|
|
@@ -197,15 +229,15 @@
|
|
|
197
229
|
<div class="bg-muted/40 border-border grid grid-cols-3 gap-2 rounded-lg border p-2.5 text-center text-xs">
|
|
198
230
|
<div>
|
|
199
231
|
<div class="text-muted-foreground font-semibold uppercase tracking-wide">Netto</div>
|
|
200
|
-
<div class="text-sm font-bold tabular-nums">{
|
|
232
|
+
<div class="text-sm font-bold tabular-nums">{formatPln(netPln)} zł</div>
|
|
201
233
|
</div>
|
|
202
234
|
<div class="border-border border-x">
|
|
203
235
|
<div class="text-muted-foreground font-semibold uppercase tracking-wide">VAT</div>
|
|
204
|
-
<div class="text-sm font-bold tabular-nums">{
|
|
236
|
+
<div class="text-sm font-bold tabular-nums">{formatPln(vatPln)} zł</div>
|
|
205
237
|
</div>
|
|
206
238
|
<div>
|
|
207
239
|
<div class="text-muted-foreground font-semibold uppercase tracking-wide">Brutto</div>
|
|
208
|
-
<div class="text-primary text-sm font-bold tabular-nums">{
|
|
240
|
+
<div class="text-primary text-sm font-bold tabular-nums">{formatPln(grossPln)} zł</div>
|
|
209
241
|
</div>
|
|
210
242
|
</div>
|
|
211
243
|
<label class="flex items-center gap-2">
|
|
@@ -226,6 +258,42 @@
|
|
|
226
258
|
{/if}
|
|
227
259
|
</section>
|
|
228
260
|
|
|
261
|
+
{#if paymentMethods.length > 0}
|
|
262
|
+
<section class="border-border bg-card space-y-4 rounded-xl border p-6">
|
|
263
|
+
<h2 class="text-lg font-bold">Dozwolone metody płatności</h2>
|
|
264
|
+
<p class="text-muted-foreground text-sm">
|
|
265
|
+
Ogranicz metody płatności dla tej dostawy (np. wyłącz płatność za pobraniem dla
|
|
266
|
+
paczkomatu). Jeśli wyłączone — wszystkie skonfigurowane metody są dostępne.
|
|
267
|
+
</p>
|
|
268
|
+
<label class="flex items-center gap-2">
|
|
269
|
+
<Switch bind:checked={restrictPayments} />
|
|
270
|
+
<span class="text-sm">Ogranicz metody płatności</span>
|
|
271
|
+
</label>
|
|
272
|
+
{#if restrictPayments}
|
|
273
|
+
<div class="space-y-2 pl-2">
|
|
274
|
+
{#each paymentMethods as p (p.id)}
|
|
275
|
+
<label class="flex items-center gap-2">
|
|
276
|
+
<input
|
|
277
|
+
type="checkbox"
|
|
278
|
+
checked={allowedPaymentIds.includes(p.id)}
|
|
279
|
+
onchange={(e) =>
|
|
280
|
+
togglePaymentMethod(p.id, (e.currentTarget as HTMLInputElement).checked)}
|
|
281
|
+
/>
|
|
282
|
+
<span class="text-sm">{paymentLabel(p)}</span>
|
|
283
|
+
<span class="text-muted-foreground text-xs">({p.id})</span>
|
|
284
|
+
</label>
|
|
285
|
+
{/each}
|
|
286
|
+
{#if allowedPaymentIds.length === 0}
|
|
287
|
+
<p class="text-xs text-amber-700">
|
|
288
|
+
Wybierz co najmniej jedną metodę, inaczej checkout dla tej dostawy będzie
|
|
289
|
+
zablokowany.
|
|
290
|
+
</p>
|
|
291
|
+
{/if}
|
|
292
|
+
</div>
|
|
293
|
+
{/if}
|
|
294
|
+
</section>
|
|
295
|
+
{/if}
|
|
296
|
+
|
|
229
297
|
<section class="border-border bg-card space-y-4 rounded-xl border p-6">
|
|
230
298
|
<h2 class="text-lg font-bold">Przewoźnik</h2>
|
|
231
299
|
<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,24 +12,27 @@ 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
|
}
|
|
14
19
|
export interface ShippingFormInitial {
|
|
15
20
|
name?: Record<string, string> | unknown;
|
|
16
21
|
description?: Record<string, string> | unknown | null;
|
|
17
|
-
price?: number;
|
|
22
|
+
price?: number | string;
|
|
18
23
|
vatRate?: number;
|
|
19
24
|
carrierType?: string;
|
|
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;
|
|
@@ -35,12 +35,15 @@
|
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
function
|
|
38
|
+
function formatPricePln(pln: number) {
|
|
39
39
|
return new Intl.NumberFormat('pl-PL', {
|
|
40
40
|
style: 'currency',
|
|
41
41
|
currency: 'PLN',
|
|
42
42
|
minimumFractionDigits: 2
|
|
43
|
-
}).format(
|
|
43
|
+
}).format(pln);
|
|
44
|
+
}
|
|
45
|
+
function formatPriceCents(cents: number) {
|
|
46
|
+
return formatPricePln(cents / 100);
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
async function doReorder(fromIndex: number, toIndex: number) {
|
|
@@ -143,11 +146,11 @@
|
|
|
143
146
|
{resolveI18n(m.name as Record<string, string>, interfaceLanguage.current, '')}
|
|
144
147
|
</a>
|
|
145
148
|
</div>
|
|
146
|
-
<div>{
|
|
149
|
+
<div>{formatPricePln(Number(m.price))}</div>
|
|
147
150
|
<div>{m.vatRate}%</div>
|
|
148
151
|
<div>
|
|
149
152
|
{#if cond?.freeAbove != null}
|
|
150
|
-
{
|
|
153
|
+
{formatPriceCents(cond.freeAbove)}
|
|
151
154
|
{:else}
|
|
152
155
|
<span class="text-muted-foreground text-xs">—</span>
|
|
153
156
|
{/if}
|
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
const entriesQuery = $derived(remotes.listShopProductEntries());
|
|
14
14
|
const collectionsQuery = $derived(remotes.listShopableCollections());
|
|
15
15
|
|
|
16
|
-
function formatPrice(
|
|
16
|
+
function formatPrice(pln: number) {
|
|
17
17
|
return new Intl.NumberFormat('pl-PL', {
|
|
18
18
|
style: 'currency',
|
|
19
19
|
currency: 'PLN',
|
|
20
20
|
minimumFractionDigits: 2
|
|
21
|
-
}).format(
|
|
21
|
+
}).format(pln);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function resolveTitle(data: Record<string, unknown> | null, fallback: string): string {
|