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.
Files changed (83) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/DOCS.md +231 -1
  3. package/ROADMAP.md +7 -2
  4. package/dist/admin/client/shop/shipping-method-edit-page.svelte +1 -0
  5. package/dist/admin/client/shop/shipping-method-form.svelte +89 -21
  6. package/dist/admin/client/shop/shipping-method-form.svelte.d.ts +8 -1
  7. package/dist/admin/client/shop/shipping-method-new-page.svelte +1 -0
  8. package/dist/admin/client/shop/shipping-methods-list-page.svelte +7 -4
  9. package/dist/admin/client/shop/shop-products-list-page.svelte +2 -2
  10. package/dist/admin/components/fields/shop-field.svelte +63 -22
  11. package/dist/admin/remote/shop.remote.d.ts +16 -56
  12. package/dist/admin/remote/shop.remote.js +6 -4
  13. package/dist/cli/scaffold/admin.js +32 -0
  14. package/dist/db-postgres/schema/shop/order.d.ts +34 -0
  15. package/dist/db-postgres/schema/shop/order.js +2 -0
  16. package/dist/db-postgres/schema/shop/product.d.ts +4 -4
  17. package/dist/db-postgres/schema/shop/product.js +3 -2
  18. package/dist/db-postgres/schema/shop/productVariant.d.ts +4 -4
  19. package/dist/db-postgres/schema/shop/productVariant.js +3 -2
  20. package/dist/db-postgres/schema/shop/shippingMethod.d.ts +23 -4
  21. package/dist/db-postgres/schema/shop/shippingMethod.js +4 -2
  22. package/dist/shop/adapters/payu/client.d.ts +22 -0
  23. package/dist/shop/adapters/payu/client.js +78 -0
  24. package/dist/shop/adapters/payu/index.d.ts +24 -0
  25. package/dist/shop/adapters/payu/index.js +88 -0
  26. package/dist/shop/adapters/payu/payload.d.ts +48 -0
  27. package/dist/shop/adapters/payu/payload.js +48 -0
  28. package/dist/shop/adapters/payu/signature.d.ts +12 -0
  29. package/dist/shop/adapters/payu/signature.js +50 -0
  30. package/dist/shop/adapters/payu/status-map.d.ts +3 -0
  31. package/dist/shop/adapters/payu/status-map.js +14 -0
  32. package/dist/shop/cart/order-token-cookie.d.ts +9 -0
  33. package/dist/shop/cart/order-token-cookie.js +40 -0
  34. package/dist/shop/client/index.d.ts +64 -1
  35. package/dist/shop/client/index.js +9 -0
  36. package/dist/shop/client/use-order.svelte.d.ts +32 -0
  37. package/dist/shop/client/use-order.svelte.js +105 -0
  38. package/dist/shop/http/checkout-handler.js +47 -4
  39. package/dist/shop/http/index.d.ts +4 -0
  40. package/dist/shop/http/index.js +4 -0
  41. package/dist/shop/http/order-handler.d.ts +4 -0
  42. package/dist/shop/http/order-handler.js +85 -0
  43. package/dist/shop/http/refresh-payment-handler.d.ts +4 -0
  44. package/dist/shop/http/refresh-payment-handler.js +73 -0
  45. package/dist/shop/http/retry-payment-handler.d.ts +4 -0
  46. package/dist/shop/http/retry-payment-handler.js +99 -0
  47. package/dist/shop/http/retry-payment-logic.d.ts +2 -0
  48. package/dist/shop/http/retry-payment-logic.js +4 -0
  49. package/dist/shop/http/shipping-handler.js +2 -1
  50. package/dist/shop/http/webhook-handler.d.ts +4 -0
  51. package/dist/shop/http/webhook-handler.js +73 -0
  52. package/dist/shop/http/webhook-logic.d.ts +4 -0
  53. package/dist/shop/http/webhook-logic.js +21 -0
  54. package/dist/shop/index.d.ts +3 -1
  55. package/dist/shop/index.js +3 -1
  56. package/dist/shop/pricing.d.ts +4 -0
  57. package/dist/shop/pricing.js +18 -0
  58. package/dist/shop/server/cart-hydrate.js +6 -3
  59. package/dist/shop/server/email.js +18 -2
  60. package/dist/shop/server/order-access-url.d.ts +7 -0
  61. package/dist/shop/server/order-access-url.js +6 -0
  62. package/dist/shop/server/orders.d.ts +1 -0
  63. package/dist/shop/server/orders.js +12 -0
  64. package/dist/shop/server/payment-compat.d.ts +5 -0
  65. package/dist/shop/server/payment-compat.js +9 -0
  66. package/dist/shop/server/populate.d.ts +2 -0
  67. package/dist/shop/server/shipping.d.ts +12 -4
  68. package/dist/shop/server/shipping.js +24 -14
  69. package/dist/shop/server/shop-data.d.ts +8 -2
  70. package/dist/shop/server/shop-data.js +18 -10
  71. package/dist/shop/svelte/OrderStatus.svelte +368 -0
  72. package/dist/shop/svelte/OrderStatus.svelte.d.ts +14 -0
  73. package/dist/shop/svelte/index.d.ts +3 -0
  74. package/dist/shop/svelte/index.js +2 -0
  75. package/dist/shop/svelte/labels.d.ts +25 -0
  76. package/dist/shop/svelte/labels.js +41 -0
  77. package/dist/shop/types.d.ts +19 -1
  78. package/dist/updates/0.15.1/index.d.ts +2 -0
  79. package/dist/updates/0.15.1/index.js +27 -0
  80. package/dist/updates/0.15.2/index.d.ts +2 -0
  81. package/dist/updates/0.15.2/index.js +18 -0
  82. package/dist/updates/index.js +3 -1
  83. package/package.json +5 -1
@@ -0,0 +1,368 @@
1
+ <script lang="ts">
2
+ import { onMount, onDestroy } from 'svelte';
3
+ import {
4
+ createOrderState,
5
+ type OrderDetailResponse,
6
+ type OrderState
7
+ } from '../client/index.js';
8
+ import { DEFAULT_LABELS_PL, type OrderStatusLabels } from './labels.js';
9
+
10
+ interface Props {
11
+ number: string;
12
+ token?: string;
13
+ initialData?: OrderDetailResponse | null;
14
+ labels?: Partial<OrderStatusLabels>;
15
+ autoPoll?: boolean;
16
+ onPaid?: (data: OrderDetailResponse) => void;
17
+ class?: string;
18
+ }
19
+
20
+ const {
21
+ number,
22
+ token,
23
+ initialData = null,
24
+ labels: labelOverrides,
25
+ autoPoll = true,
26
+ onPaid,
27
+ class: className = ''
28
+ }: Props = $props();
29
+
30
+ const labels: OrderStatusLabels = $derived({
31
+ ...DEFAULT_LABELS_PL,
32
+ ...labelOverrides,
33
+ title: { ...DEFAULT_LABELS_PL.title, ...labelOverrides?.title },
34
+ intro: { ...DEFAULT_LABELS_PL.intro, ...labelOverrides?.intro },
35
+ totals: { ...DEFAULT_LABELS_PL.totals, ...labelOverrides?.totals },
36
+ actions: { ...DEFAULT_LABELS_PL.actions, ...labelOverrides?.actions },
37
+ errors: { ...DEFAULT_LABELS_PL.errors, ...labelOverrides?.errors }
38
+ });
39
+
40
+ const order: OrderState = createOrderState({ number, token, initialData });
41
+
42
+ let lastSeenStatus: string | null = $state(initialData?.order.status ?? null);
43
+
44
+ $effect(() => {
45
+ const current = order.data?.order.status ?? null;
46
+ if (current && current !== lastSeenStatus) {
47
+ if (current === 'paid' && order.data && onPaid) onPaid(order.data);
48
+ lastSeenStatus = current;
49
+ }
50
+ });
51
+
52
+ onMount(async () => {
53
+ if (!order.data) await order.load();
54
+ if (autoPoll && (order.status === 'awaitingPayment' || order.status === 'new')) {
55
+ order.startPolling();
56
+ }
57
+ });
58
+
59
+ onDestroy(() => order.dispose());
60
+
61
+ async function handleRetry() {
62
+ const redirectUrl = await order.retry();
63
+ if (redirectUrl && typeof window !== 'undefined') {
64
+ window.location.href = redirectUrl;
65
+ }
66
+ }
67
+
68
+ async function handleRefresh() {
69
+ await order.refreshPayment();
70
+ }
71
+
72
+ function fmt(cents: number, currency: string): string {
73
+ return new Intl.NumberFormat('pl-PL', {
74
+ style: 'currency',
75
+ currency,
76
+ minimumFractionDigits: 2
77
+ }).format(cents / 100);
78
+ }
79
+
80
+ function fmtDate(iso: string): string {
81
+ const d = new Date(iso);
82
+ return d.toLocaleString('pl-PL');
83
+ }
84
+
85
+ function productName(snapshot: { product?: string; variant?: string } | null | undefined): string {
86
+ if (!snapshot) return '—';
87
+ const product = snapshot.product ?? '';
88
+ const variant = snapshot.variant;
89
+ return variant ? `${product} — ${variant}` : product || '—';
90
+ }
91
+
92
+ const canRetry = $derived(
93
+ order.data?.order.status === 'awaitingPayment' ||
94
+ order.data?.order.status === 'paymentRejected'
95
+ );
96
+ const isErrorState = $derived(
97
+ order.data?.order.status === 'paymentRejected' ||
98
+ order.data?.order.status === 'cancelled'
99
+ );
100
+ const isSuccessState = $derived(
101
+ order.data?.order.status === 'paid' ||
102
+ order.data?.order.status === 'preparing' ||
103
+ order.data?.order.status === 'sent' ||
104
+ order.data?.order.status === 'done'
105
+ );
106
+ </script>
107
+
108
+ <div class="aria-order-status {className}" part="root">
109
+ {#if order.loading && !order.data}
110
+ <div class="state-loading" part="loading" role="status" aria-live="polite">…</div>
111
+ {:else if order.error && !order.data}
112
+ <div class="state-error" part="error" role="alert">
113
+ {order.error.message.includes('Forbidden')
114
+ ? labels.errors.forbidden
115
+ : labels.errors.loadFailed}
116
+ </div>
117
+ {:else if order.data}
118
+ {@const o = order.data.order}
119
+ <article
120
+ class="card"
121
+ class:is-success={isSuccessState}
122
+ class:is-error={isErrorState}
123
+ part="card"
124
+ >
125
+ <header class="header" part="header">
126
+ <span class="status-badge" part="status-badge">{o.status}</span>
127
+ <h1 class="title" part="title">{labels.title[o.status]}</h1>
128
+ <p class="intro" part="intro">{labels.intro[o.status]}</p>
129
+ </header>
130
+
131
+ <section class="summary" part="summary">
132
+ <div class="summary-row">
133
+ <span class="summary-label">{labels.orderNumber}</span>
134
+ <span class="summary-value">{o.number}</span>
135
+ </div>
136
+ <div class="summary-row">
137
+ <span class="summary-label">Email</span>
138
+ <span class="summary-value">{o.customerEmail}</span>
139
+ </div>
140
+ <div class="summary-row">
141
+ <span class="summary-label">{labels.totals.gross}</span>
142
+ <span class="summary-value">{fmt(o.totalGross, o.currency)}</span>
143
+ </div>
144
+ </section>
145
+
146
+ {#if order.data.items.length > 0}
147
+ <section class="items" part="items">
148
+ <h2 class="section-title">{labels.items}</h2>
149
+ <ul class="items-list">
150
+ {#each order.data.items as item (item.id)}
151
+ <li class="item-row" part="item">
152
+ <span class="item-name">{productName(item.nameSnapshot)}</span>
153
+ <span class="item-qty">×{item.qty}</span>
154
+ <span class="item-price">
155
+ {fmt(item.priceGrossSnapshot * item.qty, o.currency)}
156
+ </span>
157
+ </li>
158
+ {/each}
159
+ </ul>
160
+ </section>
161
+ {/if}
162
+
163
+ {#if canRetry || order.status === 'awaitingPayment'}
164
+ <section class="actions" part="actions">
165
+ {#if canRetry}
166
+ <button
167
+ type="button"
168
+ class="btn btn-primary"
169
+ part="btn-retry"
170
+ onclick={handleRetry}
171
+ disabled={order.phase === 'retrying'}
172
+ >
173
+ {order.phase === 'retrying' ? labels.actions.retrying : labels.actions.retry}
174
+ </button>
175
+ {/if}
176
+ {#if order.status === 'awaitingPayment'}
177
+ <button
178
+ type="button"
179
+ class="btn btn-secondary"
180
+ part="btn-refresh"
181
+ onclick={handleRefresh}
182
+ disabled={order.phase === 'refreshing'}
183
+ >
184
+ {order.phase === 'refreshing' ? labels.actions.refreshing : labels.actions.refresh}
185
+ </button>
186
+ {/if}
187
+ </section>
188
+ {/if}
189
+
190
+ {#if order.data.statusHistory.length > 0}
191
+ <section class="history" part="history">
192
+ <h2 class="section-title">{labels.statusHistory}</h2>
193
+ <ol class="history-list">
194
+ {#each order.data.statusHistory as h, i (i)}
195
+ <li class="history-row" part="history-item">
196
+ <span class="history-status">{h.status}</span>
197
+ <span class="history-date">{fmtDate(h.changedAt)}</span>
198
+ {#if h.note}<span class="history-note">{h.note}</span>{/if}
199
+ </li>
200
+ {/each}
201
+ </ol>
202
+ </section>
203
+ {/if}
204
+ </article>
205
+ {/if}
206
+ </div>
207
+
208
+ <style>
209
+ .aria-order-status {
210
+ --shop-bg: #fafafe;
211
+ --shop-fg: #1a1a2e;
212
+ --shop-muted: #555566;
213
+ --shop-accent: #5b4a9e;
214
+ --shop-success: #3a8a5c;
215
+ --shop-error: #c44b4b;
216
+ --shop-border: #e2dff0;
217
+ --shop-radius: 12px;
218
+ --shop-font-display: inherit;
219
+ --shop-font-body: inherit;
220
+
221
+ background: var(--shop-bg);
222
+ color: var(--shop-fg);
223
+ font-family: var(--shop-font-body);
224
+ }
225
+
226
+ .state-loading,
227
+ .state-error {
228
+ padding: 2rem;
229
+ text-align: center;
230
+ color: var(--shop-muted);
231
+ }
232
+
233
+ .card {
234
+ max-width: 640px;
235
+ margin: 0 auto;
236
+ padding: 2rem;
237
+ border: 1px solid var(--shop-border);
238
+ border-radius: var(--shop-radius);
239
+ background: var(--shop-bg);
240
+ }
241
+
242
+ .card.is-success {
243
+ border-color: var(--shop-success);
244
+ }
245
+ .card.is-error {
246
+ border-color: var(--shop-error);
247
+ }
248
+
249
+ .header {
250
+ margin-bottom: 1.5rem;
251
+ }
252
+
253
+ .status-badge {
254
+ display: inline-block;
255
+ font-size: 0.75rem;
256
+ letter-spacing: 0.05em;
257
+ text-transform: uppercase;
258
+ color: var(--shop-muted);
259
+ }
260
+
261
+ .title {
262
+ font-family: var(--shop-font-display);
263
+ font-size: 1.75rem;
264
+ font-weight: 800;
265
+ margin: 0.25rem 0 0.5rem;
266
+ }
267
+
268
+ .intro {
269
+ color: var(--shop-muted);
270
+ margin: 0;
271
+ line-height: 1.6;
272
+ }
273
+
274
+ .summary,
275
+ .items,
276
+ .actions,
277
+ .history {
278
+ margin-top: 1.5rem;
279
+ padding-top: 1.5rem;
280
+ border-top: 1px solid var(--shop-border);
281
+ }
282
+
283
+ .section-title {
284
+ font-size: 0.875rem;
285
+ font-weight: 600;
286
+ text-transform: uppercase;
287
+ letter-spacing: 0.05em;
288
+ color: var(--shop-muted);
289
+ margin: 0 0 0.75rem;
290
+ }
291
+
292
+ .summary-row,
293
+ .history-row {
294
+ display: flex;
295
+ justify-content: space-between;
296
+ gap: 1rem;
297
+ padding: 0.4rem 0;
298
+ font-size: 0.9rem;
299
+ }
300
+
301
+ .summary-label,
302
+ .history-date {
303
+ color: var(--shop-muted);
304
+ }
305
+
306
+ .items-list {
307
+ list-style: none;
308
+ padding: 0;
309
+ margin: 0;
310
+ }
311
+
312
+ .item-row {
313
+ display: grid;
314
+ grid-template-columns: 1fr auto auto;
315
+ gap: 0.75rem;
316
+ padding: 0.4rem 0;
317
+ font-size: 0.9rem;
318
+ }
319
+
320
+ .actions {
321
+ display: flex;
322
+ gap: 0.75rem;
323
+ flex-wrap: wrap;
324
+ }
325
+
326
+ .btn {
327
+ font-family: inherit;
328
+ font-size: 0.95rem;
329
+ font-weight: 600;
330
+ padding: 0.65rem 1.2rem;
331
+ border-radius: calc(var(--shop-radius) - 4px);
332
+ cursor: pointer;
333
+ border: 1px solid transparent;
334
+ transition: opacity 0.15s;
335
+ }
336
+
337
+ .btn:disabled {
338
+ opacity: 0.6;
339
+ cursor: wait;
340
+ }
341
+
342
+ .btn-primary {
343
+ background: var(--shop-accent);
344
+ color: #fff;
345
+ }
346
+
347
+ .btn-secondary {
348
+ background: transparent;
349
+ color: var(--shop-accent);
350
+ border-color: var(--shop-accent);
351
+ }
352
+
353
+ .history-list {
354
+ list-style: none;
355
+ padding: 0;
356
+ margin: 0;
357
+ }
358
+
359
+ .history-status {
360
+ font-weight: 600;
361
+ }
362
+
363
+ .history-note {
364
+ flex-basis: 100%;
365
+ color: var(--shop-muted);
366
+ font-size: 0.8rem;
367
+ }
368
+ </style>
@@ -0,0 +1,14 @@
1
+ import { type OrderDetailResponse } from '../client/index.js';
2
+ import { type OrderStatusLabels } from './labels.js';
3
+ interface Props {
4
+ number: string;
5
+ token?: string;
6
+ initialData?: OrderDetailResponse | null;
7
+ labels?: Partial<OrderStatusLabels>;
8
+ autoPoll?: boolean;
9
+ onPaid?: (data: OrderDetailResponse) => void;
10
+ class?: string;
11
+ }
12
+ declare const OrderStatus: import("svelte").Component<Props, {}, "">;
13
+ type OrderStatus = ReturnType<typeof OrderStatus>;
14
+ export default OrderStatus;
@@ -0,0 +1,3 @@
1
+ export { default as OrderStatus } from './OrderStatus.svelte';
2
+ export { DEFAULT_LABELS_PL } from './labels.js';
3
+ export type { OrderStatusLabels } from './labels.js';
@@ -0,0 +1,2 @@
1
+ export { default as OrderStatus } from './OrderStatus.svelte';
2
+ export { DEFAULT_LABELS_PL } from './labels.js';
@@ -0,0 +1,25 @@
1
+ import type { OrderStatus } from '../types.js';
2
+ export interface OrderStatusLabels {
3
+ title: Record<OrderStatus, string>;
4
+ intro: Record<OrderStatus, string>;
5
+ orderNumber: string;
6
+ items: string;
7
+ totals: {
8
+ net: string;
9
+ vat: string;
10
+ gross: string;
11
+ shipping: string;
12
+ };
13
+ actions: {
14
+ retry: string;
15
+ refresh: string;
16
+ refreshing: string;
17
+ retrying: string;
18
+ };
19
+ errors: {
20
+ loadFailed: string;
21
+ forbidden: string;
22
+ };
23
+ statusHistory: string;
24
+ }
25
+ export declare const DEFAULT_LABELS_PL: OrderStatusLabels;
@@ -0,0 +1,41 @@
1
+ export const DEFAULT_LABELS_PL = {
2
+ title: {
3
+ new: 'Zamówienie przyjęte',
4
+ awaitingPayment: 'Oczekujemy na płatność',
5
+ paid: 'Dziękujemy!',
6
+ preparing: 'Zamówienie w przygotowaniu',
7
+ sent: 'Zamówienie wysłane',
8
+ done: 'Zamówienie zrealizowane',
9
+ cancelled: 'Zamówienie anulowane',
10
+ paymentRejected: 'Płatność nieudana'
11
+ },
12
+ intro: {
13
+ new: 'Otrzymaliśmy Twoje zamówienie.',
14
+ awaitingPayment: 'Czekamy na potwierdzenie płatności. To zwykle trwa kilka sekund, ale czasem dłużej.',
15
+ paid: 'Płatność została zaksięgowana. Przekazujemy zamówienie do realizacji.',
16
+ preparing: 'Pakujemy Twoje zamówienie.',
17
+ sent: 'Zamówienie zostało wysłane.',
18
+ done: 'Zamówienie zostało zakończone. Dziękujemy za zakupy!',
19
+ cancelled: 'To zamówienie zostało anulowane.',
20
+ paymentRejected: 'Płatność nie doszła do skutku. Możesz spróbować ponownie.'
21
+ },
22
+ orderNumber: 'Numer zamówienia',
23
+ items: 'Pozycje',
24
+ totals: {
25
+ net: 'Netto',
26
+ vat: 'VAT',
27
+ gross: 'Razem',
28
+ shipping: 'Wysyłka'
29
+ },
30
+ actions: {
31
+ retry: 'Zapłać ponownie',
32
+ refresh: 'Sprawdź status',
33
+ refreshing: 'Sprawdzanie…',
34
+ retrying: 'Przekierowuję…'
35
+ },
36
+ errors: {
37
+ loadFailed: 'Nie udało się pobrać zamówienia.',
38
+ forbidden: 'Link wygasł lub jest nieprawidłowy.'
39
+ },
40
+ statusHistory: 'Historia statusów'
41
+ };
@@ -22,11 +22,21 @@ export interface ConsentConfig {
22
22
  required: boolean;
23
23
  labelI18n: I18nText;
24
24
  }
25
+ export interface PaymentCreateContext {
26
+ customerIp?: string;
27
+ language?: string | null;
28
+ }
25
29
  export interface PaymentAdapter {
26
30
  id: string;
27
31
  label: I18nText;
28
- createPayment(order: OrderRef): Promise<PaymentCreateResult>;
32
+ createPayment(order: OrderRef, ctx?: PaymentCreateContext): Promise<PaymentCreateResult>;
29
33
  handleWebhook?(req: Request): Promise<PaymentEvent>;
34
+ /**
35
+ * Poll the provider for the latest payment status. Used as a fallback
36
+ * when a webhook is lost. `providerRef` is the external id the adapter
37
+ * returned as `PaymentCreateResult.providerRef`.
38
+ */
39
+ getStatus?(providerRef: string): Promise<PaymentEvent>;
30
40
  }
31
41
  export interface CarrierAdapter {
32
42
  id: string;
@@ -63,10 +73,18 @@ export interface ShopConfig {
63
73
  rateLimit?: ShopRateLimit;
64
74
  consents?: ConsentConfig[];
65
75
  languages?: Language[];
76
+ /**
77
+ * URL template used to build a customer-facing order view link.
78
+ * Supported placeholders: {orderNumber}, {orderId}, {accessToken}, {language}.
79
+ * May be absolute (used in emails) or relative (used in redirects).
80
+ * Default when omitted: `/shop/order/{orderNumber}?token={accessToken}`.
81
+ */
82
+ orderViewUrl?: string;
66
83
  }
67
84
  export interface ResolvedShopConfig extends ShopConfig {
68
85
  features: Required<ShopFeatures>;
69
86
  rateLimit: Required<ShopRateLimit>;
70
87
  carriers: CarrierAdapter[];
71
88
  consents: ConsentConfig[];
89
+ orderViewUrl: string;
72
90
  }
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,27 @@
1
+ export const update = {
2
+ version: '0.15.1',
3
+ date: '2026-04-15',
4
+ description: 'Shop: PayU payment adapter + secure order access (token-gated view API, email link).',
5
+ features: [
6
+ 'Orders now carry an `accessToken` — public order-view API is gated by this token (no enumeration by order number).',
7
+ '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).',
8
+ '`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.',
9
+ '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.',
10
+ '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.',
11
+ 'Orders store `paymentProviderRef` — the external id returned by the payment adapter (e.g. PayU orderId). Used to correlate webhooks and future refunds/status polls.',
12
+ '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`.',
13
+ '`PaymentAdapter.createPayment()` receives an optional `{ customerIp, language }` context, threaded from the SvelteKit request in the built-in checkout handler.',
14
+ '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.',
15
+ '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).',
16
+ '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.',
17
+ '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.',
18
+ '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`.',
19
+ '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`.',
20
+ '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`.',
21
+ 'Docs: new section covering the shop module, order view patterns (generic vs headless), and retry lifecycle.'
22
+ ],
23
+ fixes: [],
24
+ breakingChanges: [],
25
+ sql: "ALTER TABLE shop_orders ADD COLUMN access_token uuid NOT NULL DEFAULT gen_random_uuid();\nALTER TABLE shop_orders ADD COLUMN payment_provider_ref text;\nALTER TABLE shop_shipping_methods ADD COLUMN allowed_payment_methods jsonb;",
26
+ notes: '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.'
27
+ };
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,18 @@
1
+ export const update = {
2
+ version: '0.15.2',
3
+ date: '2026-04-15',
4
+ description: 'Shop: cena przechowywana jako numeric(20,6) — eliminacja driftu brutto/netto po reload; toggle netto/brutto per wariant.',
5
+ features: [
6
+ 'Warianty produktu mają teraz toggle netto/brutto obok pola "Zmiana ceny" — spójne z toggle\'em ceny bazowej. Delta zapisywana kanonicznie jako netto.'
7
+ ],
8
+ fixes: [
9
+ '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.',
10
+ 'Shipping: ta sama poprawka dla ceny metody wysyłki (stored jako netto PLN z 6dp).'
11
+ ],
12
+ breakingChanges: [
13
+ '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.',
14
+ '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.'
15
+ ],
16
+ sql: 'ALTER TABLE shop_products ALTER COLUMN base_price TYPE numeric(20,6) USING (base_price::numeric / 100);\nALTER TABLE shop_product_variants ALTER COLUMN price_delta TYPE numeric(20,6) USING (price_delta::numeric / 100);\nALTER TABLE shop_shipping_methods ALTER COLUMN price TYPE numeric(20,6) USING (price::numeric / 100);',
17
+ notes: '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).'
18
+ };
@@ -45,7 +45,9 @@ import { update as update0144 } from './0.14.4/index.js';
45
45
  import { update as update0145 } from './0.14.5/index.js';
46
46
  import { update as update0146 } from './0.14.6/index.js';
47
47
  import { update as update0150 } from './0.15.0/index.js';
48
- export const updates = [update0065, update0066, update0067, update0068, update0069, update010, update011, update012, update013, update014, update015, update020, update022, update050, update051, update052, update053, update054, update055, update056, update057, update058, update060, update061, update062, update070, update071, update072, update073, update080, update090, update0100, update0110, update0120, update0130, update0131, update0132, update0133, update0134, update0140, update0141, update0142, update0143, update0144, update0145, update0146, update0150];
48
+ import { update as update0151 } from './0.15.1/index.js';
49
+ import { update as update0152 } from './0.15.2/index.js';
50
+ export const updates = [update0065, update0066, update0067, update0068, update0069, update010, update011, update012, update013, update014, update015, update020, update022, update050, update051, update052, update053, update054, update055, update056, update057, update058, update060, update061, update062, update070, update071, update072, update073, update080, update090, update0100, update0110, update0120, update0130, update0131, update0132, update0133, update0134, update0140, update0141, update0142, update0143, update0144, update0145, update0146, update0150, update0151, update0152];
49
51
  export const getUpdatesFrom = (fromVersion) => {
50
52
  const fromParts = fromVersion.split('.').map(Number);
51
53
  return updates.filter((update) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "includio-cms",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",
@@ -151,6 +151,10 @@
151
151
  "types": "./dist/shop/http/index.d.ts",
152
152
  "node": "./dist/shop/http/index.js"
153
153
  },
154
+ "./shop/svelte": {
155
+ "types": "./dist/shop/svelte/index.d.ts",
156
+ "svelte": "./dist/shop/svelte/index.js"
157
+ },
154
158
  "./files-local": {
155
159
  "types": "./dist/files-local/index.d.ts",
156
160
  "node": "./dist/files-local/index.js"