includio-cms 0.26.0 → 0.28.0

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 (128) hide show
  1. package/API.md +58 -2
  2. package/CHANGELOG.md +105 -0
  3. package/DOCS.md +1 -1
  4. package/ROADMAP.md +8 -0
  5. package/dist/admin/auth-client.d.ts +42 -42
  6. package/dist/admin/client/admin/admin-layout.svelte +12 -2
  7. package/dist/admin/client/admin/admin-layout.svelte.d.ts +2 -1
  8. package/dist/admin/client/collection/data-table.svelte +0 -39
  9. package/dist/admin/client/collection/data-table.svelte.d.ts +0 -2
  10. package/dist/admin/client/shop/coupon-schema.d.ts +1 -1
  11. package/dist/admin/client/shop/refund-dialog.svelte +37 -1
  12. package/dist/admin/client/shop/refund-dialog.svelte.d.ts +3 -0
  13. package/dist/admin/client/shop/shop-order-detail-page.svelte +192 -0
  14. package/dist/admin/components/fields/field-renderer.svelte +6 -1
  15. package/dist/admin/components/fields/icon-field.svelte +86 -0
  16. package/dist/admin/components/fields/icon-field.svelte.d.ts +8 -0
  17. package/dist/admin/components/fields/icon-picker-dialog.svelte +174 -0
  18. package/dist/admin/components/fields/icon-picker-dialog.svelte.d.ts +11 -0
  19. package/dist/admin/components/fields/object-field.svelte +27 -7
  20. package/dist/admin/components/fields/shop-field.svelte +210 -20
  21. package/dist/admin/components/layout/layout-tabs.svelte +1 -0
  22. package/dist/admin/components/variant-form/VariantAttributeRenderer.svelte +109 -0
  23. package/dist/admin/components/variant-form/VariantAttributeRenderer.svelte.d.ts +9 -0
  24. package/dist/admin/helpers/build-icon-set-map.d.ts +8 -0
  25. package/dist/admin/helpers/build-icon-set-map.js +16 -0
  26. package/dist/admin/helpers/index.d.ts +2 -0
  27. package/dist/admin/helpers/index.js +2 -0
  28. package/dist/admin/remote/shop.remote.d.ts +116 -24
  29. package/dist/admin/remote/shop.remote.js +79 -6
  30. package/dist/admin/state/icon-sets.svelte.d.ts +9 -0
  31. package/dist/admin/state/icon-sets.svelte.js +20 -0
  32. package/dist/cli/scaffold/admin.js +2 -2
  33. package/dist/components/ui/checkbox/checkbox.svelte +3 -3
  34. package/dist/core/cms.d.ts +11 -2
  35. package/dist/core/cms.js +29 -0
  36. package/dist/core/fields/fieldSchemaToTs.js +7 -0
  37. package/dist/core/server/generator/fields.d.ts +2 -0
  38. package/dist/core/server/generator/fields.js +34 -1
  39. package/dist/core/server/generator/generator.js +2 -1
  40. package/dist/db-postgres/schema/shop/index.d.ts +1 -0
  41. package/dist/db-postgres/schema/shop/index.js +1 -0
  42. package/dist/db-postgres/schema/shop/invoice.d.ts +254 -0
  43. package/dist/db-postgres/schema/shop/invoice.js +27 -0
  44. package/dist/db-postgres/schema/shop/order.d.ts +107 -1
  45. package/dist/db-postgres/schema/shop/order.js +7 -1
  46. package/dist/db-postgres/schema/shop/payment.d.ts +20 -0
  47. package/dist/db-postgres/schema/shop/payment.js +4 -1
  48. package/dist/db-postgres/schema/shop/product.d.ts +20 -0
  49. package/dist/db-postgres/schema/shop/product.js +3 -1
  50. package/dist/db-postgres/schema/shop/productVariant.d.ts +12 -2
  51. package/dist/db-postgres/schema/shop/productVariant.js +22 -0
  52. package/dist/paraglide/messages/_index.d.ts +36 -3
  53. package/dist/paraglide/messages/_index.js +71 -3
  54. package/dist/paraglide/messages/en.d.ts +5 -0
  55. package/dist/paraglide/messages/en.js +14 -0
  56. package/dist/paraglide/messages/pl.d.ts +5 -0
  57. package/dist/paraglide/messages/pl.js +14 -0
  58. package/dist/shop/adapters/fakturownia/client.d.ts +28 -0
  59. package/dist/shop/adapters/fakturownia/client.js +67 -0
  60. package/dist/shop/adapters/fakturownia/index.d.ts +27 -0
  61. package/dist/shop/adapters/fakturownia/index.js +36 -0
  62. package/dist/shop/adapters/fakturownia/payload.d.ts +35 -0
  63. package/dist/shop/adapters/fakturownia/payload.js +45 -0
  64. package/dist/shop/cart/types.d.ts +1 -0
  65. package/dist/shop/client/index.d.ts +61 -0
  66. package/dist/shop/client/index.js +5 -1
  67. package/dist/shop/expiry.d.ts +35 -0
  68. package/dist/shop/expiry.js +68 -0
  69. package/dist/shop/http/balance-handler.d.ts +20 -0
  70. package/dist/shop/http/balance-handler.js +91 -0
  71. package/dist/shop/http/cart-handler.js +19 -0
  72. package/dist/shop/http/checkout-handler.js +30 -1
  73. package/dist/shop/http/index.d.ts +2 -0
  74. package/dist/shop/http/index.js +2 -0
  75. package/dist/shop/http/upcoming-handler.d.ts +16 -0
  76. package/dist/shop/http/upcoming-handler.js +65 -0
  77. package/dist/shop/http/webhook-handler.js +46 -9
  78. package/dist/shop/index.d.ts +7 -1
  79. package/dist/shop/index.js +10 -1
  80. package/dist/shop/nip.d.ts +12 -0
  81. package/dist/shop/nip.js +23 -0
  82. package/dist/shop/server/balance-payment.d.ts +40 -0
  83. package/dist/shop/server/balance-payment.js +140 -0
  84. package/dist/shop/server/cart-hydrate.js +2 -0
  85. package/dist/shop/server/init.d.ts +14 -0
  86. package/dist/shop/server/init.js +35 -0
  87. package/dist/shop/server/invoices.d.ts +64 -0
  88. package/dist/shop/server/invoices.js +237 -0
  89. package/dist/shop/server/orders.d.ts +38 -0
  90. package/dist/shop/server/orders.js +152 -2
  91. package/dist/shop/server/payment-policy.d.ts +35 -0
  92. package/dist/shop/server/payment-policy.js +55 -0
  93. package/dist/shop/server/payments.d.ts +29 -0
  94. package/dist/shop/server/payments.js +64 -0
  95. package/dist/shop/server/populate.d.ts +1 -1
  96. package/dist/shop/server/refund.d.ts +17 -12
  97. package/dist/shop/server/refund.js +96 -13
  98. package/dist/shop/server/shop-data.d.ts +4 -1
  99. package/dist/shop/server/shop-data.js +24 -2
  100. package/dist/shop/template.d.ts +13 -0
  101. package/dist/shop/template.js +98 -0
  102. package/dist/shop/types.d.ts +208 -1
  103. package/dist/shop/variant-attributes.d.ts +28 -0
  104. package/dist/shop/variant-attributes.js +69 -0
  105. package/dist/sveltekit/server/index.d.ts +1 -0
  106. package/dist/sveltekit/server/index.js +2 -0
  107. package/dist/types/cms.d.ts +4 -3
  108. package/dist/types/cms.schema.d.ts +1 -1
  109. package/dist/types/cms.schema.js +9 -0
  110. package/dist/types/fields.d.ts +21 -2
  111. package/dist/types/index.d.ts +1 -1
  112. package/dist/types/index.js +1 -1
  113. package/dist/types/plugins.d.ts +40 -0
  114. package/dist/types/plugins.js +4 -1
  115. package/dist/updates/0.26.1/index.d.ts +2 -0
  116. package/dist/updates/0.26.1/index.js +19 -0
  117. package/dist/updates/0.27.0/index.d.ts +2 -0
  118. package/dist/updates/0.27.0/index.js +50 -0
  119. package/dist/updates/0.28.0/index.d.ts +2 -0
  120. package/dist/updates/0.28.0/index.js +38 -0
  121. package/dist/updates/index.js +7 -1
  122. package/package.json +1 -1
  123. package/dist/paraglide/messages/hello_world.d.ts +0 -5
  124. package/dist/paraglide/messages/hello_world.js +0 -33
  125. package/dist/paraglide/messages/login_hello.d.ts +0 -16
  126. package/dist/paraglide/messages/login_hello.js +0 -34
  127. package/dist/paraglide/messages/login_please_login.d.ts +0 -16
  128. package/dist/paraglide/messages/login_please_login.js +0 -34
@@ -147,11 +147,194 @@ export interface CouponRef {
147
147
  value: number;
148
148
  discountAmount: number;
149
149
  }
150
+ /**
151
+ * Schema descriptor for one product-variant attribute (city, startsAt, …).
152
+ * Drives Zod validation, ts-gen typings, GIN indexes, and admin renderers.
153
+ * @public
154
+ */
155
+ export type VariantAttribute = VariantAttributeText | VariantAttributeNumber | VariantAttributeDatetime | VariantAttributeSelect | VariantAttributeBoolean | VariantAttributeImage | VariantAttributeEntry | VariantAttributeSlug;
156
+ interface VariantAttributeBase {
157
+ label: I18nText;
158
+ required?: boolean;
159
+ /**
160
+ * Generate a `GIN ((attributes->'<key>'))` index on the variants table.
161
+ * Enable for keys filtered on storefront (`city`, `startsAt`, …).
162
+ */
163
+ indexable?: boolean;
164
+ }
165
+ /** @public */
166
+ export interface VariantAttributeText extends VariantAttributeBase {
167
+ type: 'text';
168
+ }
169
+ /** @public */
170
+ export interface VariantAttributeNumber extends VariantAttributeBase {
171
+ type: 'number';
172
+ }
173
+ /** @public — value stored as ISO-8601 string. */
174
+ export interface VariantAttributeDatetime extends VariantAttributeBase {
175
+ type: 'datetime';
176
+ }
177
+ /** @public */
178
+ export interface VariantAttributeSelect extends VariantAttributeBase {
179
+ type: 'select';
180
+ options: {
181
+ value: string;
182
+ label: I18nText;
183
+ }[];
184
+ }
185
+ /** @public */
186
+ export interface VariantAttributeBoolean extends VariantAttributeBase {
187
+ type: 'boolean';
188
+ }
189
+ /** @public — value = media file id (UUID). */
190
+ export interface VariantAttributeImage extends VariantAttributeBase {
191
+ type: 'image';
192
+ }
193
+ /** @public — value = entry id (UUID) from given collection. */
194
+ export interface VariantAttributeEntry extends VariantAttributeBase {
195
+ type: 'entry';
196
+ entryCollection: string;
197
+ }
198
+ /** @public — value matches slug regex `^[a-z0-9-]+$`. */
199
+ export interface VariantAttributeSlug extends VariantAttributeBase {
200
+ type: 'slug';
201
+ }
202
+ /**
203
+ * Template used to auto-generate `variant.name` from `variantAttributes`.
204
+ * Syntax: `{key|filter:arg}`. Filters: `date` (long|medium|short),
205
+ * `currency` (currency code), `uppercase`. Admin pre-fills the name input
206
+ * with the interpolated string; once the editor types into the name field,
207
+ * pre-fill stops for that variant (per-session dirty flag).
208
+ * @public
209
+ */
210
+ export interface VariantLabelConfig {
211
+ template: string;
212
+ }
213
+ /**
214
+ * Per-product payment policy. `full` (default) charges the full order total
215
+ * immediately; `deposit` charges only a deposit at checkout and leaves a
216
+ * remaining balance owed by the customer, redeemable via a signed balance
217
+ * link sent by the admin. Set on `ShopFieldData.paymentPolicy` per product
218
+ * entry — not globally on `defineShop`.
219
+ * @public
220
+ */
221
+ export type PaymentPolicy = {
222
+ type: 'full';
223
+ } | {
224
+ type: 'deposit';
225
+ depositAmount: DepositAmount;
226
+ };
227
+ /**
228
+ * Deposit amount specifier. `percent` charges `floor(base * value / 100)` of
229
+ * the line total; `amount` charges a fixed minor-unit value (clamped to the
230
+ * line total). Validation: `value > 0` always; `percent` ≤ 100; `amount` in
231
+ * the same minor-unit as `order.totalGross` (grosze for PLN).
232
+ * @public
233
+ */
234
+ export type DepositAmount = {
235
+ type: 'percent';
236
+ value: number;
237
+ } | {
238
+ type: 'amount';
239
+ value: number;
240
+ };
241
+ /**
242
+ * Persisted partial-payment summary on `order.partialPayment` when the order
243
+ * was placed under a deposit policy. Amounts are in minor currency units
244
+ * (grosze for PLN). `paidAt` is the ISO timestamp when the deposit cleared
245
+ * (or `null` while still pending). On balance payment, `paidAmount` is
246
+ * bumped to the full order total and `paidAt` is refreshed.
247
+ * @public
248
+ */
249
+ export interface PartialPayment {
250
+ kind: 'deposit';
251
+ paidAmount: number;
252
+ balanceAmount: number;
253
+ paidAt: string | null;
254
+ }
255
+ /**
256
+ * Opt-in storefront filter that hides variants whose datetime attribute
257
+ * (`source`, e.g. `startsAt`) has already passed. `offsetDays` shifts the
258
+ * cut-off: `0` = expire exactly when the source datetime hits now, `1` = one
259
+ * day after (grace period), `-1` = one day before. Admin still shows expired
260
+ * variants with a "Zakończony" badge; storefront `listUpcoming` filters them
261
+ * out and cart/checkout reject them with `VARIANT_EXPIRED`.
262
+ * @public
263
+ */
264
+ export interface VariantExpiryConfig {
265
+ source: string;
266
+ offsetDays: number;
267
+ }
268
+ /**
269
+ * When an invoice should be issued automatically after an order is fully paid.
270
+ * - `b2bAndOnRequest` (default): only when the order carries a NIP (B2B) or the
271
+ * customer explicitly requested an invoice (`invoiceRequested`).
272
+ * - `always`: for every fully-paid order regardless of NIP / request.
273
+ * @public
274
+ */
275
+ export type InvoiceIssuePolicy = 'b2bAndOnRequest' | 'always';
276
+ /** @public */
277
+ export interface InvoiceBuyer {
278
+ name: string;
279
+ email: string;
280
+ nip?: string | null;
281
+ companyName?: string | null;
282
+ address?: Record<string, string> | null;
283
+ }
284
+ /** @public */
285
+ export interface InvoiceLineItem {
286
+ name: string;
287
+ quantity: number;
288
+ /** Gross unit price in minor units (grosze for PLN). */
289
+ unitPriceGross: number;
290
+ /** VAT rate as a plain percentage (e.g. `23` for 23%). */
291
+ vatRate: number;
292
+ }
293
+ /** @public */
294
+ export interface InvoicePayload {
295
+ orderNumber: string;
296
+ currency: Currency;
297
+ buyer: InvoiceBuyer;
298
+ items: InvoiceLineItem[];
299
+ /** ISO timestamp the payment was confirmed — used as issue / paid date. */
300
+ paidAt: string;
301
+ }
302
+ /** @public */
303
+ export interface InvoiceCreateResult {
304
+ externalId: string;
305
+ number?: string;
306
+ pdfUrl?: string;
307
+ raw?: unknown;
308
+ }
309
+ /** @public */
310
+ export interface InvoiceContext {
311
+ language?: string | null;
312
+ }
313
+ /**
314
+ * Adapter that issues invoices with an external provider (e.g. Fakturownia).
315
+ * Registered via `defineShop({ invoicing })`. `createInvoice` is called once an
316
+ * order is fully paid and qualifies under `issueWhen`; `send` (optional) e-mails
317
+ * the issued invoice to the buyer provider-side.
318
+ * @public
319
+ */
320
+ export interface InvoicingAdapter {
321
+ id: string;
322
+ /** Trigger policy. Default `b2bAndOnRequest`. */
323
+ issueWhen?: InvoiceIssuePolicy;
324
+ createInvoice(payload: InvoicePayload, ctx?: InvoiceContext): Promise<InvoiceCreateResult>;
325
+ send?(externalId: string, ctx?: InvoiceContext): Promise<void>;
326
+ }
150
327
  export interface ShopConfig {
151
328
  currency: Currency;
152
329
  vatRates: number[];
153
330
  payment: PaymentAdapter[];
154
331
  carriers?: CarrierAdapter[];
332
+ /**
333
+ * Optional invoicing adapter — issues invoices automatically once an order is
334
+ * fully paid. Omitted = no invoicing.
335
+ * @public
336
+ */
337
+ invoicing?: InvoicingAdapter | null;
155
338
  features?: ShopFeatures;
156
339
  rateLimit?: ShopRateLimit;
157
340
  consents?: ConsentConfig[];
@@ -168,11 +351,35 @@ export interface ShopConfig {
168
351
  * automatic admin notifications. Omitted = those alerts are no-ops.
169
352
  */
170
353
  adminEmail?: string;
354
+ /**
355
+ * Typed variant attribute schema. Each key declares one attribute (city,
356
+ * startsAt, …) — drives Zod validation, ts-gen typings, GIN indexes, and
357
+ * admin variant form renderer. Omitted = legacy untyped string map.
358
+ * @public
359
+ */
360
+ variantAttributes?: Record<string, VariantAttribute>;
361
+ /**
362
+ * Template used by admin to auto-prefill `variant.name` from typed
363
+ * `variantAttributes`. Omitted = no pre-fill (editor enters name manually).
364
+ * @public
365
+ */
366
+ variantLabel?: VariantLabelConfig;
367
+ /**
368
+ * Opt-in storefront filter for time-bound variants (events, courses).
369
+ * Omitted = no filtering (legacy behavior — every variant always listed).
370
+ * @public
371
+ */
372
+ variantExpiry?: VariantExpiryConfig;
171
373
  }
172
- export interface ResolvedShopConfig extends ShopConfig {
374
+ export interface ResolvedShopConfig extends Omit<ShopConfig, 'variantLabel' | 'variantExpiry' | 'invoicing'> {
173
375
  features: Required<ShopFeatures>;
174
376
  rateLimit: Required<ShopRateLimit>;
175
377
  carriers: CarrierAdapter[];
176
378
  consents: ConsentConfig[];
379
+ invoicing: InvoicingAdapter | null;
177
380
  orderViewUrl: string;
381
+ variantAttributes: Record<string, VariantAttribute>;
382
+ variantLabel: VariantLabelConfig | null;
383
+ variantExpiry: VariantExpiryConfig | null;
178
384
  }
385
+ export {};
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod';
2
+ import type { VariantAttribute } from './types.js';
3
+ /**
4
+ * Build a Zod object schema from a `defineShop({ variantAttributes })` map.
5
+ * Required attributes are mandatory keys; optional ones use `.optional()`.
6
+ * Empty map → `z.object({})`.
7
+ * @internal
8
+ */
9
+ export declare function buildVariantAttributesSchema(attrs: Record<string, VariantAttribute>): z.ZodObject<Record<string, z.ZodType>>;
10
+ /**
11
+ * @public
12
+ * Thrown by `upsertShopData` (and `validateVariantAttributes`) when a
13
+ * `variant.attributes` payload fails the schema derived from
14
+ * `defineShop({ variantAttributes })`. Carries the raw Zod issues.
15
+ */
16
+ export declare class InvalidVariantAttributesError extends Error {
17
+ readonly code = "INVALID_VARIANT_ATTRIBUTES";
18
+ readonly issues: z.ZodIssue[];
19
+ constructor(issues: z.ZodIssue[]);
20
+ }
21
+ /**
22
+ * Validate a variant attributes payload against a shop config.
23
+ * Throws `InvalidVariantAttributesError` (code `INVALID_VARIANT_ATTRIBUTES`) on
24
+ * failure. Returns the parsed (and coerced where applicable) value on success.
25
+ * `null` / `undefined` payloads are treated as `{}`.
26
+ * @internal
27
+ */
28
+ export declare function validateVariantAttributes(value: unknown, attrs: Record<string, VariantAttribute>): Record<string, unknown>;
@@ -0,0 +1,69 @@
1
+ import { z } from 'zod';
2
+ const SLUG_REGEX = /^[a-z0-9-]+$/;
3
+ function buildLeafSchema(attr) {
4
+ switch (attr.type) {
5
+ case 'text':
6
+ return attr.required ? z.string().min(1) : z.string();
7
+ case 'number':
8
+ return z.number().finite();
9
+ case 'datetime':
10
+ // Accept full ISO 8601: UTC (`...Z`) AND offset forms (`+02:00`).
11
+ // Admin DatetimeField produces `Z` via toISOString(); imports/seeds
12
+ // may use local offsets. Both are valid ISO 8601.
13
+ return z.string().datetime({ offset: true });
14
+ case 'select':
15
+ return z.enum(attr.options.map((o) => o.value));
16
+ case 'boolean':
17
+ return z.boolean();
18
+ case 'image':
19
+ case 'entry':
20
+ return z.string().uuid();
21
+ case 'slug':
22
+ return z.string().regex(SLUG_REGEX);
23
+ }
24
+ }
25
+ /**
26
+ * Build a Zod object schema from a `defineShop({ variantAttributes })` map.
27
+ * Required attributes are mandatory keys; optional ones use `.optional()`.
28
+ * Empty map → `z.object({})`.
29
+ * @internal
30
+ */
31
+ export function buildVariantAttributesSchema(attrs) {
32
+ const shape = {};
33
+ for (const [key, attr] of Object.entries(attrs)) {
34
+ const leaf = buildLeafSchema(attr);
35
+ shape[key] = attr.required ? leaf : leaf.optional();
36
+ }
37
+ return z.object(shape);
38
+ }
39
+ /**
40
+ * @public
41
+ * Thrown by `upsertShopData` (and `validateVariantAttributes`) when a
42
+ * `variant.attributes` payload fails the schema derived from
43
+ * `defineShop({ variantAttributes })`. Carries the raw Zod issues.
44
+ */
45
+ export class InvalidVariantAttributesError extends Error {
46
+ code = 'INVALID_VARIANT_ATTRIBUTES';
47
+ issues;
48
+ constructor(issues) {
49
+ super(`Invalid variant attributes: ${issues.map((i) => i.message).join('; ')}`);
50
+ this.name = 'InvalidVariantAttributesError';
51
+ this.issues = issues;
52
+ }
53
+ }
54
+ /**
55
+ * Validate a variant attributes payload against a shop config.
56
+ * Throws `InvalidVariantAttributesError` (code `INVALID_VARIANT_ATTRIBUTES`) on
57
+ * failure. Returns the parsed (and coerced where applicable) value on success.
58
+ * `null` / `undefined` payloads are treated as `{}`.
59
+ * @internal
60
+ */
61
+ export function validateVariantAttributes(value, attrs) {
62
+ const schema = buildVariantAttributesSchema(attrs);
63
+ const input = value ?? {};
64
+ const result = schema.safeParse(input);
65
+ if (!result.success) {
66
+ throw new InvalidVariantAttributesError(result.error.issues);
67
+ }
68
+ return result.data;
69
+ }
@@ -7,3 +7,4 @@ export { createConsentLog } from '../../core/server/consentLogs/operations/creat
7
7
  export { getPreviewEntry } from './preview.js';
8
8
  export { createRestApiHandler } from '../../admin/api/rest/handler.js';
9
9
  export { generateApiKey } from '../../admin/api/rest/middleware/generateApiKey.js';
10
+ export { createAdminApiHandler } from '../../admin/api/handler.js';
@@ -8,3 +8,5 @@ export { getPreviewEntry } from './preview.js';
8
8
  // Folded from `./admin/api/rest/handler` (dropped as separate export in 0.20.0)
9
9
  export { createRestApiHandler } from '../../admin/api/rest/handler.js';
10
10
  export { generateApiKey } from '../../admin/api/rest/middleware/generateApiKey.js';
11
+ // Folded from `./admin/api/handler` (dropped as separate export in 0.26.1)
12
+ export { createAdminApiHandler } from '../../admin/api/handler.js';
@@ -3,7 +3,7 @@ import type { FilesAdapter } from './adapters/files.js';
3
3
  import type { CollectionConfig, CollectionConfigWithType } from './collections.js';
4
4
  import type { Language } from './languages.js';
5
5
  import type { SingleConfig, SingleConfigWithType } from './singles.js';
6
- import type { CustomFieldDefinition, PluginConfig } from './plugins.js';
6
+ import type { CustomFieldDefinition, IconSetPlugin, Plugin } from './plugins.js';
7
7
  import type { FormConfig } from './forms.js';
8
8
  import type { AIAdapter } from './adapters/ai.js';
9
9
  import type { EmailAdapter } from './adapters/email.js';
@@ -77,7 +77,7 @@ export interface CMSConfig {
77
77
  files: FilesAdapter;
78
78
  email?: EmailAdapter;
79
79
  auth?: AuthConfig;
80
- plugins?: PluginConfig[];
80
+ plugins?: Plugin[];
81
81
  ai?: AIAdapter;
82
82
  media?: MediaConfig;
83
83
  apiKeys?: ApiKeyConfig[];
@@ -95,8 +95,9 @@ export interface ICMS {
95
95
  filesAdapter: FilesAdapter;
96
96
  emailAdapter: EmailAdapter | null;
97
97
  authConfig: AuthConfig | null;
98
- plugins: PluginConfig[];
98
+ plugins: Plugin[];
99
99
  customFields: Map<string, CustomFieldDefinition>;
100
+ iconSets: Map<string, IconSetPlugin>;
100
101
  aiAdapter: AIAdapter | null;
101
102
  mediaConfig: MediaConfig;
102
103
  apiKeys: ApiKeyConfig[];
@@ -445,7 +445,7 @@ export declare const _internal: {
445
445
  shop: z.ZodOptional<z.ZodUnknown>;
446
446
  cmp: z.ZodOptional<z.ZodUnknown>;
447
447
  }, z.core.$loose>;
448
- FIELD_TYPES: readonly ["text", "content", "number", "boolean", "date", "datetime", "file", "media", "select", "radio", "checkboxes", "relation", "object", "array", "blocks", "slug", "seo", "shop", "url", "custom"];
448
+ FIELD_TYPES: readonly ["text", "content", "number", "boolean", "date", "datetime", "file", "media", "select", "radio", "checkboxes", "relation", "object", "array", "blocks", "slug", "seo", "shop", "url", "icon", "custom"];
449
449
  FORM_FIELD_TYPES: readonly ["text", "email", "textarea", "checkbox", "select", "file"];
450
450
  getLangCode: typeof getLangCode;
451
451
  };
@@ -51,6 +51,7 @@ const FIELD_TYPES = [
51
51
  'seo',
52
52
  'shop',
53
53
  'url',
54
+ 'icon',
54
55
  'custom'
55
56
  ];
56
57
  const baseFieldShape = {
@@ -269,6 +270,14 @@ const fieldSchema = z.lazy(() => z.discriminatedUnion('type', [
269
270
  type: z.literal('custom'),
270
271
  fieldType: z.string().min(1, { message: 'custom.fieldType is required' }),
271
272
  config: z.record(z.string(), z.unknown()).optional()
273
+ })
274
+ .passthrough(),
275
+ // icon — set provided by IconSetPlugin (registered in plugins[])
276
+ z
277
+ .object({
278
+ ...baseFieldShape,
279
+ type: z.literal('icon'),
280
+ set: z.string().optional()
272
281
  })
273
282
  .passthrough()
274
283
  ]));
@@ -2,7 +2,7 @@ import type { FormatEnum } from 'sharp';
2
2
  import type { ImageStyle, MediaFile, VideoStyle } from './media.js';
3
3
  import type { Localized } from './languages.js';
4
4
  import type { StructuredContentDoc } from './structured-content.js';
5
- export type FieldType = 'text' | 'content' | 'number' | 'boolean' | 'date' | 'datetime' | 'file' | 'media' | 'select' | 'radio' | 'checkboxes' | 'relation' | 'object' | 'array' | 'blocks' | 'slug' | 'seo' | 'shop' | 'url' | 'custom';
5
+ export type FieldType = 'text' | 'content' | 'number' | 'boolean' | 'date' | 'datetime' | 'file' | 'media' | 'select' | 'radio' | 'checkboxes' | 'relation' | 'object' | 'array' | 'blocks' | 'slug' | 'seo' | 'shop' | 'url' | 'icon' | 'custom';
6
6
  export interface FieldCondition {
7
7
  field: string;
8
8
  equals?: string | string[];
@@ -234,6 +234,11 @@ export interface ShopFieldData {
234
234
  basePrice: number;
235
235
  vatRate: number;
236
236
  isActive: boolean;
237
+ /**
238
+ * Per-product payment policy. Omitted = `{ type: 'full' }` (legacy behavior).
239
+ * @public
240
+ */
241
+ paymentPolicy?: import('../shop/types.js').PaymentPolicy;
237
242
  variants?: Array<{
238
243
  id?: string;
239
244
  sku?: string | null;
@@ -264,4 +269,18 @@ export interface CustomField extends BaseField {
264
269
  /** Plugin-specific configuration, opaque to core */
265
270
  config?: Record<string, unknown>;
266
271
  }
267
- export type Field = TextField | ContentField | NumberField | BooleanField | DateField | DateTimeField | FileField | MediaField | SelectField | RadioField | CheckboxesField | RelationField | ObjectField | ArrayField | BlocksField | SlugField | SeoField | ShopField | UrlField | CustomField;
272
+ /**
273
+ * @public
274
+ * Icon picker field. Stores a string key referencing an icon registered by an
275
+ * {@link import('./plugins.js').IconSetPlugin}. Admin UI renders a kafelek
276
+ * (collapsed) and opens a searchable grid dialog. The set of icons (Svelte
277
+ * components + labels) is provided by a plugin at admin bootstrap — NOT inline
278
+ * in `cms.config.ts` (which is server-only / Zod-validated).
279
+ */
280
+ export interface IconField extends BaseField {
281
+ type: 'icon';
282
+ /** Plugin slug to source icons from when multiple icon-set plugins registered. Defaults to the first one. */
283
+ set?: string;
284
+ defaultValue?: string;
285
+ }
286
+ export type Field = TextField | ContentField | NumberField | BooleanField | DateField | DateTimeField | FileField | MediaField | SelectField | RadioField | CheckboxesField | RelationField | ObjectField | ArrayField | BlocksField | SlugField | SeoField | ShopField | UrlField | IconField | CustomField;
@@ -10,7 +10,7 @@ export { type SingleConfig } from './singles.js';
10
10
  export { type FormConfig, type FormSubmission } from './forms.js';
11
11
  export { type FormField, type FormFieldType, type FormBaseField, type FormTextField, type FormEmailField, type FormTextareaField, type FormCheckboxField, type FormSelectField } from './formFields.js';
12
12
  export { type CMSConfig, type ApiKeyConfig, type AuthConfig, type TypographyConfig } from './cms.js';
13
- export { type PluginConfig, type CustomFieldDefinition } from './plugins.js';
13
+ export { type PluginConfig, type CustomFieldDefinition, type IconSetPlugin, type IconDefinition, type Plugin, isIconSetPlugin } from './plugins.js';
14
14
  export { type Language, type Localized } from './languages.js';
15
15
  export { type Layout, type LayoutNode, type LayoutPreset, type LayoutNodeType, type ColumnRatio, type SectionNode, type ColumnsNode, type CardNode, type AccordionNode, type StackNode, type TabsNode, type TabNode } from './layout.js';
16
16
  export { type CmsContext } from './cms-context.js';
@@ -10,7 +10,7 @@ export {} from './singles.js';
10
10
  export {} from './forms.js';
11
11
  export {} from './formFields.js';
12
12
  export {} from './cms.js';
13
- export {} from './plugins.js';
13
+ export { isIconSetPlugin } from './plugins.js';
14
14
  export {} from './languages.js';
15
15
  export {} from './layout.js';
16
16
  export {} from './cms-context.js';
@@ -2,6 +2,7 @@ import type { Component } from 'svelte';
2
2
  import type { z } from 'zod';
3
3
  import type { RawEntry } from './entries.js';
4
4
  import type { CustomField } from './fields.js';
5
+ import type { Localized } from './languages.js';
5
6
  import type { PopulateCtx } from '../core/server/entries/operations/resolveEntry.js';
6
7
  /**
7
8
  * Defines a custom field type contributed by a plugin.
@@ -40,3 +41,42 @@ export interface PluginConfig {
40
41
  afterDelete?: (id: string) => Promise<void>;
41
42
  };
42
43
  }
44
+ /**
45
+ * @public
46
+ * Single icon entry contributed by an {@link IconSetPlugin}. Carries a Svelte
47
+ * component, a localized label and optional search keywords. Components are
48
+ * eagerly imported (tree-shakable) — for small sets (10–50 icons) lazy import
49
+ * adds overhead without benefit.
50
+ */
51
+ export interface IconDefinition {
52
+ /** Svelte icon component (e.g. `Brain` from `@lucide/svelte`). */
53
+ component: Component;
54
+ /** Localized display label shown in collapsed state and in the picker grid. */
55
+ label: Localized;
56
+ /** Extra search keywords (non-localized). Search matches key + label + keywords. */
57
+ keywords?: string[];
58
+ }
59
+ /**
60
+ * @public
61
+ * Icon set plugin — registers a named set of icons consumable by the `icon`
62
+ * field type. Registered alongside other plugins in `cms.config.ts`'s
63
+ * `plugins:` array. Because Svelte components do NOT serialize to the client,
64
+ * the application's admin layout must wire the same plugin instance to
65
+ * `AdminLayout` via `buildIconSetMap(...)` — see admin/helpers.
66
+ */
67
+ export interface IconSetPlugin {
68
+ /** Unique slug — referenced by `IconField.set` when multiple sets exist. */
69
+ slug: string;
70
+ type: 'icon-set';
71
+ icons: Record<string, IconDefinition>;
72
+ }
73
+ /**
74
+ * @public
75
+ * Union of all plugin kinds accepted in `cms.config.ts → plugins:`. New
76
+ * plugin kinds must be added here. Type discriminated by:
77
+ * - {@link IconSetPlugin} → `type === 'icon-set'`
78
+ * - {@link PluginConfig} → everything else (no `type` field).
79
+ */
80
+ export type Plugin = PluginConfig | IconSetPlugin;
81
+ /** Type guard for {@link IconSetPlugin}. */
82
+ export declare function isIconSetPlugin(p: Plugin): p is IconSetPlugin;
@@ -1 +1,4 @@
1
- export {};
1
+ /** Type guard for {@link IconSetPlugin}. */
2
+ export function isIconSetPlugin(p) {
3
+ return p.type === 'icon-set';
4
+ }
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,19 @@
1
+ export const update = {
2
+ version: '0.26.1',
3
+ date: '2026-05-28',
4
+ description: 'Nowy typ pola `icon` + IconSetPlugin (zestaw ikon dostarczany przez aplikację, dialog z gridem + search). Scaffolder fix + DataTable polish: scaffold admin używa publicznych entry points; pierwsza kolumna tabeli bez sticky-first; checkbox visual wraca do 16 px z transparent hit-area 24×24 (WCAG 2.5.5).',
5
+ features: [
6
+ 'Nowy typ pola `icon` (`IconField` @public, `src/lib/types/fields.ts`) — kompaktowy kafelek 96×96 w formularzu (ikona + nazwa, X do wyczyszczenia) otwiera dialog z gridem responsive 3–6 kolumn, wyszukiwarką (filtruje po `key`, lokalizowanym `label`, opcjonalnych `keywords`) i przyciskami Anuluj / OK. Klik na kafelku w dialogu zaznacza, OK zatwierdza; dwuklik = szybki zapis. Stan "missing" (wartość w DB nieobecna w bibliotece) pokazuje placeholder + przyjazne ostrzeżenie zamiast resetować wartość — zgodne z ToV "informuj, nie strasz". i18n PL/EN, `role="listbox"`/`role="option"`/`aria-selected`, LiveRegion z liczbą wyników.',
7
+ '`IconSetPlugin` / `IconDefinition` (`@public`, `src/lib/types/plugins.ts`) — nowy rodzaj pluginu rejestrujący nazwany zestaw ikon: `{ slug, type: \'icon-set\', icons: Record<key, { component, label: Localized, keywords?: string[] }> }`. Komponenty Svelte (eager import, tree-shakable — np. `Brain` z `@lucide/svelte`) ŻYJĄ w pluginie, bo `cms.config.ts` jest server-only i Zod-validated. `Plugin = PluginConfig | IconSetPlugin` + `isIconSetPlugin` type guard.',
8
+ '`buildIconSetMap(...plugins)` (`includio-cms/admin`) + nowy prop `iconSets?: Map<string, IconSetPlugin>` na `AdminLayout` — konsumencki `+layout.svelte` admina przekazuje plugin instancje na klienta (funkcji komponentów nie da się serializować przez SvelteKit data). `getIconSets()` / `resolveIconSet(slug?)` w `includio-cms/admin` — runtime lookup używany przez `icon-field.svelte` przez Svelte context.',
9
+ '`CMS.iconSets: Map<slug, IconSetPlugin>` agregowane przy starcie z `cms.config.plugins` przez `isIconSetPlugin` guard; duplikat slug → throw. Generator TS mapuje `icon` → `string` w wygenerowanym `types.ts`; Zod runtime → `z.string()` (analogicznie do `slug` field).'
10
+ ],
11
+ fixes: [
12
+ '`scaffold admin` generuje `src/routes/admin/api/[...path]/+server.ts` z `import { createAdminApiHandler } from \'includio-cms/sveltekit/server\'`. Wcześniej generowany import `includio-cms/admin/api/handler` rzucał Vite `Missing "./admin/api/handler" specifier in "includio-cms" package` przy starcie dev/build — `admin/api/handler` nie ma w `exports` (v1.0 frozen 18 ścieżek).',
13
+ '`scaffold admin` generuje `src/routes/admin/api/rest/[...restPath]/+server.ts` z `import { createRestApiHandler } from \'includio-cms/sveltekit/server\'` (analogicznie — był broken w 0.20.0+ ale scaffolder nie zaktualizowany).',
14
+ '`includio-cms/sveltekit/server` re-eksportuje teraz `createAdminApiHandler` (obok istniejącego `createRestApiHandler`).',
15
+ '`DataTable`: revert sticky-first column (z 0.26.0 / S10c) — pierwsza kolumna scrolluje razem z resztą, bez wyróżnienia kolorystycznego. Usunięty prop `stickyFirstColumn` i powiązany CSS.',
16
+ '`Checkbox` primitive: visual wraca do 16×16 px (z 24×24 w 0.26.0 / S10b), klikalny obszar utrzymany na ≥24×24 przez transparent `::after` pseudo-element (`after:-inset-1`) — WCAG 2.5.5 target-size AA dalej spełniony, lepszy wygląd zgodny z proporcjami pól tekstowych.'
17
+ ],
18
+ breakingChanges: []
19
+ };
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,50 @@
1
+ export const update = {
2
+ version: '0.27.0',
3
+ date: '2026-05-29',
4
+ description: 'Shop platformowy — Fazy 1 + 2 + 3 + 4 + 5: schema-driven `variantAttributes` (typed `defineShop` schema, Zod walidacja, GIN index helper, ts-gen typed `variant.attributes`) + admin renderer per attribute type + `variantLabel.template` interpolacja z admin auto-prefill nazwy wariantu + `variantExpiry` opt-in (storefront filter, cart/checkout guard, admin "Zakończony" badge) + `paymentPolicy` A-lite (deposit + balance + signed token + refund per kind + admin balance link + per-kind refund dialog). Faza 6 envet pilot (consumer-side end-to-end QA: szkolenia variants + deposit flow + balance link — nie wymaga zmian w core). Post-Faza-5 hot-fixy uwzględnione w tym wydaniu (single release, nie patch): datetime walidator z offsetem, auto-detect deposit kind przy admin manual mark-paid, admin paymentPolicy UI + preserve-on-undefined w upsert. Additive only — żadnych breaking zmian. Plan: `/Users/patryk/.claude/plans/implementacja-ariacms-0-27-partitioned-kahn.md`.',
5
+ features: [
6
+ '`defineShop({ variantAttributes })` (`includio-cms/shop`) — typed schema dla atrybutów wariantu: `text | number | datetime | select | boolean | image | entry | slug` z polami `{ type, label, required?, indexable?, options?, entryCollection? }`. Zostaje jedno źródło prawdy: napędza Zod walidację, ts-gen typings, GIN index utility oraz renderer admin form.',
7
+ '`VariantAttribute` + per-type interfejsy (`VariantAttributeText/Number/Datetime/Select/Boolean/Image/Entry/Slug`) wyeksportowane jako `@public` z `includio-cms/shop`.',
8
+ '`InvalidVariantAttributesError` (code `INVALID_VARIANT_ATTRIBUTES`) — rzucany przez `upsertShopData` gdy `variant.attributes` nie pasują do schemy. Eksport publiczny z `includio-cms/shop`.',
9
+ '`runtime/types.ts` — pole `shop` w entry typings generuje teraz typed `variant.attributes` (`{ city: string; startsAt: string; online?: boolean; level: \'basic\' | \'advanced\' }`) zamiast `Record<string, string>` gdy `defineShop` ma `variantAttributes`. Brak `variantAttributes` = poprzednie `Record<string, string> | null` (backwards-compatible).',
10
+ '`createVariantAttributeIndexes(attrs)` w `$lib/db-postgres/schema/shop/productVariant.ts` — zwraca `CREATE INDEX IF NOT EXISTS shop_variant_attr_<key>_gin_idx ON shop_product_variants USING gin ((attributes->\'<key>\'))` DDL per atrybut z `indexable: true`. Wymusza safe SQL identifier dla klucza (`[A-Za-z][A-Za-z0-9_]*`).',
11
+ '`applyVariantAttributeIndexes(shop, db)` w `$lib/shop/server/init.ts` — idempotentny serialized applier indeksów GIN. `initCMS()` odpala go automatycznie fire-and-forget gdy shop jest skonfigurowany (zero ręcznych hooków u konsumenta). `cms.shopInitPromise` wystawiony jako `@internal` punkt synchronizacji dla testów.',
12
+ '`VariantAttributeRenderer.svelte` (NEW, `$lib/admin/components/variant-form/`) — switch po `attr.type`, deleguje do field components z `$lib/admin/components/fields/` (text/number/datetime/select/boolean) lub renderuje prosty Input dla slug/image/entry (pełny picker UI dla image/entry defer post-1.0).',
13
+ '`shop-field.svelte` (admin variant editor) — variant row pokazuje siatkę atrybutów (responsywna 1/2-col) ponad legacy polami (nazwa/SKU/delta/stock) gdy `shopConfig.variantAttributes` ma wpisy. Brak attrs = legacy behavior bez zmian. Hydratacja + save propaguje `variant.attributes` jako `Record<string, unknown>`.',
14
+ '`getShopConfig` remote (admin) — response zawiera teraz `variantAttributes` z `ResolvedShopConfig`, dzięki czemu `shop-field` może lokalnie renderować typed atrybuty bez dodatkowego round-tripa.',
15
+ '`upsertShopDataForEntry` (admin remote command) — input schema dla `variant.attributes` zluzowany do `z.record(z.string(), z.unknown())`; typed shape egzekwowany serwerowo przez `validateVariantAttributes` (jedno źródło prawdy).',
16
+ '`defineShop({ variantLabel: { template } })` (`includio-cms/shop`) — opcjonalny szablon nazwy wariantu w składni `{key|filter:arg}`. Filtry: `date` (long/medium/short), `currency` (kod waluty, domyślnie PLN), `uppercase`. Nieznany klucz → pusty string + dev warn; nieznany filtr → passthrough; malformed template → surowy template + warn.',
17
+ '`interpolateTemplate(template, vars, locale)` (`includio-cms/shop`, `@public`) — silnik interpolacji wykorzystywany wewnętrznie przez admin do pre-fill `variant.name`, dostępny też publicznie dla konsumentów (np. storefront-side derived labels).',
18
+ '`VariantLabelConfig` eksportowany jako `@public` typ z `includio-cms/shop`.',
19
+ '`getShopConfig` remote response zawiera teraz `variantLabel: shop.variantLabel ?? null`.',
20
+ '`shop-field.svelte` (admin variant editor) — gdy `variantLabel.template` zdefiniowany, pole `Nazwa` auto-wypełnia się z interpolacji `variantAttributes` (locale `pl` w 0.27; multi-locale defer post-1.0). Per-variant dirty flag: po hydratacji z niepustym `name` lock = aktywny (nie nadpisujemy editor decision); ręczna edycja w polu `Nazwa` (`oninput`) trwale wyłącza pre-fill dla wariantu w bieżącej sesji.',
21
+ '`defineShop({ variantExpiry: { source, offsetDays } })` (`includio-cms/shop`, `@public`) — opt-in filtr wariantów time-bound (events, courses, szkolenia). `source` = klucz atrybutu typu `datetime`; `offsetDays` przesuwa cut-off (`0` = expire dokładnie gdy datetime hits now, `1` = jeden dzień grace, `-1` = jeden dzień przed). Globalny config — brak per-product override.',
22
+ '`isVariantExpired(variant, config, now?)` + `filterUpcoming(variants, config, now?)` (`includio-cms/shop`, `@public`) — fail-open helpers: `null` config / brak source attribute / non-string / malformed datetime → variant traktowany jako upcoming (z `console.warn` dla malformed w dev). `filterUpcoming` zachowuje order; sort source-asc realizowany na poziomie HTTP / admin osobno.',
23
+ '`VariantExpiredError` (code `VARIANT_EXPIRED`, `variantId`) — eksport `@public`. Rzucany przez `createOrderFromCart` w `server/orders.ts` przed stock reservation (idempotent guard).',
24
+ '`POST /api/shop/cart` (add) — preflight `VARIANT_EXPIRED` guard: kiedy `variantExpiry` jest skonfigurowany i variant ma expired source attribute, response 400 JSON `{ error: "VARIANT_EXPIRED", variantId }` bez aktualizacji cart cookie.',
25
+ '`createUpcomingVariantsHandler` (`includio-cms/shop/http`, `@experimental`) — nowy GET endpoint `/api/shop/products/[id]/variants/upcoming`. Zwraca `{ items: VariantPublic[] }` przefiltrowanych przez `filterUpcoming` + posortowanych ascendingly po `attributes.<source>` (Date.parse) gdy `variantExpiry` ustawiony, w insertion order gdy brak.',
26
+ '`client.products.listUpcoming(productId)` + `VariantPublic` (`includio-cms/shop/client`, `@public`) — nowy namespace SDK. Zwraca upcoming variants per produkt; zero-config passthrough kiedy `variantExpiry` nieustawiony.',
27
+ '`getShopConfig` remote (admin) — response zawiera teraz `variantExpiry: shop.variantExpiry ?? null`.',
28
+ '`shop-field.svelte` (admin variant editor) — gdy `variantExpiry` skonfigurowany: per-variant badge "Zakończony" (greyed-out, `bg-muted text-muted-foreground` z border), checkbox filter "Pokaż zakończone (N)" w toolbarze wariantów (default off, ukryty gdy zero expired), opacity-60 na expired row, sort wariantów po source ASC (closer terms first). Brak config = zero zmian w UI.',
29
+ '`PaymentPolicy` + `DepositAmount` + `PartialPayment` (`includio-cms/shop`, `@public`) — per-product policy `{ type: "full" } | { type: "deposit", depositAmount: { type: "percent" | "amount", value } }` ustawiana w `ShopFieldData.paymentPolicy`. Cart deposit-aware: order zachowuje pełen `totalGross`, `partialPayment.balanceAmount` = co zostaje do dopłaty, `balanceOwed = true` po zapłaconym depozycie. Walidacja: percent w (0, 100], amount = positive integer (minor units). `validatePaymentPolicy` wymusza to przy upsert produktu.',
30
+ '`resolvePaymentAmount(policy, lineGross)` + `computeDepositAmount(spec, base)` (`$lib/shop/server/payment-policy.ts`, server-internal — nie eksportowane z publicznego entry pointu) — silnik resolver per linia. `percent` flooruje `base * value / 100`; `amount` klampuje do `base`. Null / `full` policy → pełen `lineGross`, `kind = "full"`, balance 0. 15 spec testów + 10 integration.',
31
+ '`MixedPaymentPolicyError` (code `MIXED_PAYMENT_POLICY`) — `createOrderFromCart` odrzuca cart z więcej niż jednym produktem gdy którykolwiek z nich ma deposit policy. Decyzja: deposit orders span ONE product (refund-per-kind + balance link unambiguous). Cart all-`full` mixed-product nadal działa.',
32
+ '`shop_orders.partial_payment jsonb + balance_owed boolean` — nowe kolumny. `shop_payments.kind enum ("full" | "deposit" | "balance")` z default `"full"` — back-compat dla istniejących płatności. `shop_products.payment_policy jsonb` przechowuje per-product policy.',
33
+ '`shop_payments` zaczyna być aktywnie wypełniane: `checkout-handler` po `adapter.createPayment` insert payment row z odpowiednim `kind` (full lub deposit) + `providerRef`. Webhook handler matchuje (provider, providerRef) → branchuje per kind: `full`/`deposit` przepuszcza `updateOrderStatus(paid, paymentKind)`, `balance` bypasses status flow i woła `markBalancePaid` (stock przy depozycie już permanentnie potwierdzony).',
34
+ '`createOrderFromCart` CreateOrderResult: nowe pola `amountToPay` (deposit-aware kwota do pobrania na checkout) + `paymentKind` (`"full" | "deposit"`). `checkout-handler` przekazuje `amountToPay` do adaptera przez OrderRef.totalGross override — adaptery PayU/Stripe/manual działają bez zmian.',
35
+ '`generateBalanceToken` + `verifyBalanceToken` (`includio-cms/shop/server`, `@internal`) — HMAC-SHA256 nad JSON payload `{orderId, type: "balance"}`, encoding `base64url(payload).base64url(sig)`. Deterministic per (orderId, secret) — brak rotation, brak exp, server-side invalidation przez `order.balanceOwed === true`. Secret = `INCLUDIO_BALANCE_TOKEN_SECRET` env (≥16 chars, hard-fail przy brak gdy deposit policy aktywne). 8 spec testów (roundtrip, tampered, wrong orderId/secret, malformed, deterministic).',
36
+ '`createBalanceSession(orderNumber, token, ctx?)` (`includio-cms/shop/server`, `@experimental`) — customer flow: verify token + `balanceOwed`, reuse `order.paymentMethod` adapter, call `adapter.createPayment` z `amount = partialPayment.balanceAmount`, insert payment row `kind: "balance"`, mirror providerRef na order. `markBalancePaid` (idempotent) clears balanceOwed + bumps `paidAmount` do `totalGross`.',
37
+ '`createBalanceHandler` (`includio-cms/shop/http`, `@experimental`) — GET `/api/shop/orders/[number]/balance?token=...` zwraca minimalny widok ({orderNumber, currency, totalGross, amountToPay, paidAmount, paymentMethod, language}); POST inicjuje payment session ({status, redirectUrl, requiresPaymentRedirect}). 403 przy złym tokenie lub wyczyszczonym `balanceOwed`.',
38
+ '`client.orders.payBalance(orderNumber, token)` + `PayBalanceResult` (`includio-cms/shop/client`, `@public` / `@experimental`) — REST wrapper dla customer flow dopłaty.',
39
+ '`refundOrder({ kind?, releaseStock?, ... })` (`includio-cms/shop/server`) — per-kind routing. `kind: "deposit" | "balance" | "full"` matchuje konkretny payment row; kwota cap = `row.amount - prior refunds`. Domyślnie `kind` = `"full"` z heurystyką: pojedynczy paid row → użyj go, multi-row → wybierz `full`, brak rows → legacy fallback do `order.paymentProviderRef`. Refund row link do `paymentId` dla per-row audit. `getCollectedAmount` decyduje o transition do `refunded` (sum paid rows lub `totalGross` dla legacy). `releaseStock: true` (opt-in) reinkrementuje variant.stock per order item. 6 integration spec.',
40
+ '`refundOrderCmd` (admin remote) dostaje `kind` + `releaseStock` w schemacie input. `refund-dialog.svelte` — radio "Która płatność?" (deposit/balance) widoczne gdy `hasPartial`, balance radio disabled gdy `balanceOwed=true` (nic do refund), checkbox "Zwolnij stan magazynowy".',
41
+ '`generateBalanceLinkForOrder` (admin remote) — auth-protected, requires `INCLUDIO_BALANCE_TOKEN_SECRET`, builds URL z configured `orderViewUrl` + `?balance=1&balanceToken=...`. Zwraca `{ success, url, balanceAmount }`.',
42
+ '`shop-order-detail-page.svelte` — nowa sekcja "Płatność dzielona" widoczna gdy `order.partialPayment != null`: Plum badge "Czeka na dopłatę" przy `balanceOwed=true` / success badge "Opłacone w całości" po dopłacie, paid/owed/total breakdown z `formatCentsPrice`, button "Wyślij link do dopłaty" generuje token + kopiuje URL do clipboard, wyświetla URL w `<code>` blocku jako fallback dla "skopiuj ręcznie".'
43
+ ],
44
+ fixes: [
45
+ '`variant-attributes.ts` — `datetime` walidator używa teraz `z.string().datetime({ offset: true })`: akceptuje pełen ISO 8601 (`Z` UTC z admin `DatetimeField.toISOString()` ORAZ offset `+02:00` z importów/seedów/zewnętrznych systemów). Wcześniej tryb default odrzucał offset, blokując offsetowe wartości atrybutów.',
46
+ '`updateOrderStatus` (`server/orders.ts`) — admin manual mark-paid auto-detektuje `deposit` kind: gdy `order.partialPayment.kind === "deposit"` + `!balanceOwed` + `paidAt == null`, `effectiveKind = "deposit"`. Wcześniej `updateOrderStatusCmd` (w przeciwieństwie do webhooka) nie przekazywał `paymentKind`, więc admin oznaczając deposit order jako paid pomijał deposit logic (`paidAmount` nie był bumpowany, `balanceOwed` nie był ustawiany). Pozwala testować pełny deposit flow przez manual adapter bez sandbox PayU; webhook bez zmian (zawsze pass explicit kind).',
47
+ 'admin `paymentPolicy` — trzy bugi powodujące kasowanie deposit policy przy każdym save naprawione razem: (1) `upsertShopData` (`server/shop-data.ts`) — `undefined` zachowuje istniejącą policy, tylko `null` kasuje (było `?? null` nadpisujące przy każdym upsert innego pola); (2) `shopDataInputSchema` (admin remote) — dodany discriminated union `paymentPolicy` (full/deposit + percent/amount), wcześniej command odrzucał pole; (3) `shop-field.svelte` — sekcja "Zasady płatności" (radio full/deposit + warunkowe inputy percent/amount + live preview kwoty depozytu), wcześniej brak UI do ustawienia deposit.'
48
+ ],
49
+ breakingChanges: []
50
+ };
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;