includio-cms 0.24.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +29 -6
- package/CHANGELOG.md +95 -0
- package/DOCS.md +80 -5
- package/ROADMAP.md +1 -0
- package/dist/admin/client/index.d.ts +3 -0
- package/dist/admin/client/index.js +3 -0
- package/dist/admin/client/shop/coupon-edit-page.svelte +44 -0
- package/dist/admin/client/shop/coupon-edit-page.svelte.d.ts +3 -0
- package/dist/admin/client/shop/coupon-form.svelte +170 -0
- package/dist/admin/client/shop/coupon-form.svelte.d.ts +18 -0
- package/dist/admin/client/shop/coupon-new-page.svelte +25 -0
- package/dist/admin/client/shop/coupon-new-page.svelte.d.ts +18 -0
- package/dist/admin/client/shop/coupons-list-page.svelte +135 -0
- package/dist/admin/client/shop/coupons-list-page.svelte.d.ts +3 -0
- package/dist/admin/client/shop/refund-dialog.svelte +161 -0
- package/dist/admin/client/shop/refund-dialog.svelte.d.ts +11 -0
- package/dist/admin/client/shop/shipping-method-edit-page.svelte +3 -6
- package/dist/admin/client/shop/shipping-method-form.svelte +15 -21
- package/dist/admin/client/shop/shipping-method-new-page.svelte +3 -6
- package/dist/admin/client/shop/shipping-methods-list-page.svelte +6 -6
- package/dist/admin/client/shop/shop-order-detail-page.svelte +107 -27
- package/dist/admin/client/shop/shop-orders-list-page.svelte +49 -11
- package/dist/admin/client/shop/shop-products-list-page.svelte +12 -11
- package/dist/admin/components/layout/lang.d.ts +1 -0
- package/dist/admin/components/layout/lang.js +4 -2
- package/dist/admin/components/layout/layout-renderer.svelte +12 -11
- package/dist/admin/components/layout/nav-breadcrumbs.svelte +3 -5
- package/dist/admin/components/layout/nav-shop.svelte +3 -1
- package/dist/admin/components/layout/nav-user.svelte +6 -4
- package/dist/admin/components/layout/site-header.svelte +11 -5
- package/dist/admin/remote/shop.remote.d.ts +122 -3
- package/dist/admin/remote/shop.remote.js +161 -5
- package/dist/db-postgres/schema/shop/couponRedemptions.d.ts +97 -0
- package/dist/db-postgres/schema/shop/couponRedemptions.js +21 -0
- package/dist/db-postgres/schema/shop/coupons.d.ts +197 -0
- package/dist/db-postgres/schema/shop/coupons.js +18 -0
- package/dist/db-postgres/schema/shop/index.d.ts +4 -0
- package/dist/db-postgres/schema/shop/index.js +4 -0
- package/dist/db-postgres/schema/shop/product.d.ts +17 -0
- package/dist/db-postgres/schema/shop/product.js +2 -0
- package/dist/db-postgres/schema/shop/refunds.d.ts +214 -0
- package/dist/db-postgres/schema/shop/refunds.js +21 -0
- package/dist/db-postgres/schema/shop/webhookEvents.d.ts +183 -0
- package/dist/db-postgres/schema/shop/webhookEvents.js +22 -0
- package/dist/shop/adapters/payu/client.d.ts +9 -0
- package/dist/shop/adapters/payu/client.js +29 -0
- package/dist/shop/adapters/payu/index.js +17 -1
- package/dist/shop/adapters/stripe/index.d.ts +64 -0
- package/dist/shop/adapters/stripe/index.js +169 -0
- package/dist/shop/adapters/stripe/payload.d.ts +38 -0
- package/dist/shop/adapters/stripe/payload.js +90 -0
- package/dist/shop/adapters/stripe/status-map.d.ts +11 -0
- package/dist/shop/adapters/stripe/status-map.js +31 -0
- package/dist/shop/cart/coupon-cookie.d.ts +7 -0
- package/dist/shop/cart/coupon-cookie.js +32 -0
- package/dist/shop/cart/types.d.ts +12 -0
- package/dist/shop/client/index.d.ts +118 -0
- package/dist/shop/client/index.js +39 -1
- package/dist/shop/http/cart-handler.d.ts +8 -0
- package/dist/shop/http/cart-handler.js +60 -1
- package/dist/shop/http/checkout-handler.js +7 -3
- package/dist/shop/http/index.d.ts +1 -1
- package/dist/shop/http/index.js +1 -1
- package/dist/shop/http/retry-payment-handler.js +1 -1
- package/dist/shop/http/webhook-handler.js +19 -1
- package/dist/shop/http/webhook-idempotency.d.ts +16 -0
- package/dist/shop/http/webhook-idempotency.js +51 -0
- package/dist/shop/http/webhook-logic.js +2 -1
- package/dist/shop/index.d.ts +3 -1
- package/dist/shop/index.js +3 -1
- package/dist/shop/pricing.d.ts +15 -0
- package/dist/shop/pricing.js +22 -0
- package/dist/shop/server/cart-hydrate.d.ts +1 -0
- package/dist/shop/server/cart-hydrate.js +58 -10
- package/dist/shop/server/coupons.d.ts +53 -0
- package/dist/shop/server/coupons.js +117 -0
- package/dist/shop/server/email.d.ts +15 -0
- package/dist/shop/server/email.js +46 -3
- package/dist/shop/server/orders.d.ts +1 -0
- package/dist/shop/server/orders.js +120 -54
- package/dist/shop/server/refund.d.ts +32 -0
- package/dist/shop/server/refund.js +140 -0
- package/dist/shop/svelte/InpostPicker.svelte +4 -7
- package/dist/shop/svelte/OrderStatus.svelte +6 -10
- package/dist/shop/svelte/labels.js +4 -2
- package/dist/shop/types.d.ts +41 -1
- package/dist/updates/0.25.0/index.d.ts +2 -0
- package/dist/updates/0.25.0/index.js +89 -0
- package/dist/updates/index.js +64 -1
- package/package.json +6 -1
package/API.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# includio-cms — Public API v0.24.
|
|
1
|
+
# includio-cms — Public API v0.24.1
|
|
2
2
|
|
|
3
3
|
> Auto-generated by `scripts/generate-api-md.ts`. Do not edit by hand.
|
|
4
4
|
|
|
5
|
-
Entry points: **15** · Stable: **
|
|
5
|
+
Entry points: **15** · Stable: **369** · Experimental: **2**
|
|
6
6
|
|
|
7
7
|
Tags:
|
|
8
8
|
- `@public` — frozen for v1.0; semver-protected.
|
|
@@ -150,6 +150,9 @@ Tags:
|
|
|
150
150
|
- `const ShippingMethodEditPage: LegacyComponentType`
|
|
151
151
|
- `const ShippingMethodNewPage: LegacyComponentType`
|
|
152
152
|
- `const ShippingMethodsListPage: LegacyComponentType`
|
|
153
|
+
- `const ShopCouponEditPage: LegacyComponentType`
|
|
154
|
+
- `const ShopCouponNewPage: LegacyComponentType`
|
|
155
|
+
- `const ShopCouponsListPage: LegacyComponentType`
|
|
153
156
|
- `const ShopOrderDetailPage: LegacyComponentType`
|
|
154
157
|
- `const ShopOrdersListPage: LegacyComponentType`
|
|
155
158
|
- `const ShopProductsListPage: LegacyComponentType`
|
|
@@ -167,10 +170,12 @@ Tags:
|
|
|
167
170
|
- `const bulkSetMediaFileTags: <inferred>`
|
|
168
171
|
- `const cancelShipmentForOrderCmd: <inferred>`
|
|
169
172
|
- `const countMediaFiles: <inferred>`
|
|
173
|
+
- `const createCouponCmd: <inferred>`
|
|
170
174
|
- `const createEntry: <inferred>`
|
|
171
175
|
- `const createMediaTag: <inferred>`
|
|
172
176
|
- `const createShipmentForOrderCmd: <inferred>`
|
|
173
177
|
- `const createShippingMethodCmd: <inferred>`
|
|
178
|
+
- `const deleteCouponCmd: <inferred>`
|
|
174
179
|
- `const deleteEntryCommand: <inferred>`
|
|
175
180
|
- `const deleteFormSubmission: <inferred>`
|
|
176
181
|
- `const deleteFormSubmissions: <inferred>`
|
|
@@ -179,11 +184,13 @@ Tags:
|
|
|
179
184
|
- `const deleteShippingMethodCmd: <inferred>`
|
|
180
185
|
- `const deleteShopDataForEntry: <inferred>`
|
|
181
186
|
- `const exportFormSubmissions: <inferred>`
|
|
187
|
+
- `const exportOrdersCsv: <inferred>`
|
|
182
188
|
- `const findMediaReferences: <inferred>`
|
|
183
189
|
- `const generateAltText: <inferred>`
|
|
184
190
|
- `const getAltOverview: <inferred>`
|
|
185
191
|
- `const getCollection: <inferred>`
|
|
186
192
|
- `const getCollections: <inferred>`
|
|
193
|
+
- `const getCouponAdmin: <inferred>`
|
|
187
194
|
- `const getEmailConfigured: <inferred>`
|
|
188
195
|
- `const getEntries: <inferred>`
|
|
189
196
|
- `const getEntry: <inferred>`
|
|
@@ -201,6 +208,7 @@ Tags:
|
|
|
201
208
|
- `const getMediaTags: <inferred>`
|
|
202
209
|
- `const getMediaTagsWithCounts: <inferred>`
|
|
203
210
|
- `const getOrderForAdmin: <inferred>`
|
|
211
|
+
- `const getOrderRefundsAdmin: <inferred>`
|
|
204
212
|
- `const getRawEntries: <inferred>`
|
|
205
213
|
- `const getRawEntry: <inferred>`
|
|
206
214
|
- `const getRecentActivity: <inferred>`
|
|
@@ -213,11 +221,13 @@ Tags:
|
|
|
213
221
|
- `const getSingles: <inferred>`
|
|
214
222
|
- `const getSubmissionsOverview: <inferred>`
|
|
215
223
|
- `const isAIAvailable: <inferred>`
|
|
224
|
+
- `const listCouponsAdmin: <inferred>`
|
|
216
225
|
- `const listOrdersAdmin: <inferred>`
|
|
217
226
|
- `const listShippingMethodsAdmin: <inferred>`
|
|
218
227
|
- `const listShopableCollections: <inferred>`
|
|
219
228
|
- `const listShopProductEntries: <inferred>`
|
|
220
229
|
- `const populatePreviewData: <inferred>`
|
|
230
|
+
- `const refundOrderCmd: <inferred>`
|
|
221
231
|
- `const regenerateVideoPoster: <inferred>`
|
|
222
232
|
- `const renameMediaFile: <inferred>`
|
|
223
233
|
- `const reorderEntriesCommand: <inferred>`
|
|
@@ -227,6 +237,7 @@ Tags:
|
|
|
227
237
|
- `const setMediaFileAlt: <inferred>`
|
|
228
238
|
- `const setMediaFileTags: <inferred>`
|
|
229
239
|
- `const unarchiveEntryCommand: <inferred>`
|
|
240
|
+
- `const updateCouponCmd: <inferred>`
|
|
230
241
|
- `const updateEntryCommand: <inferred>`
|
|
231
242
|
- `const updateEntryVersionCommand: <inferred>`
|
|
232
243
|
- `const updateFormSubmission: <inferred>`
|
|
@@ -305,6 +316,9 @@ Tags:
|
|
|
305
316
|
- `const sessionRelations: <inferred>`
|
|
306
317
|
- `interface ShippingCarrierConfig`
|
|
307
318
|
- `type ShopCarrierType = 'none' | 'inpost' | string`
|
|
319
|
+
- `const shopCouponRedemptionsTable: <inferred>`
|
|
320
|
+
- `const shopCouponsTable: <inferred>`
|
|
321
|
+
- `type ShopCouponType = 'percent' | 'fixed'`
|
|
308
322
|
- `const shopOrderItemsTable: <inferred>`
|
|
309
323
|
- `const shopOrdersTable: <inferred>`
|
|
310
324
|
- `const shopOrderStatusHistoryTable: <inferred>`
|
|
@@ -312,8 +326,11 @@ Tags:
|
|
|
312
326
|
- `type ShopPaymentStatus = 'pending' | 'paid' | 'failed' | 'refunded' | 'cancelled'`
|
|
313
327
|
- `const shopProductsTable: <inferred>`
|
|
314
328
|
- `const shopProductVariantsTable: <inferred>`
|
|
329
|
+
- `const shopRefundsTable: <inferred>`
|
|
330
|
+
- `type ShopRefundStatus = 'pending' | 'succeeded' | 'failed'`
|
|
315
331
|
- `const shopShippingMethodsTable: <inferred>`
|
|
316
332
|
- `const shopStockReservationsTable: <inferred>`
|
|
333
|
+
- `const shopWebhookEventsTable: <inferred>`
|
|
317
334
|
- `const user: <inferred>`
|
|
318
335
|
- `const userRelations: <inferred>`
|
|
319
336
|
- `const verification: <inferred>`
|
|
@@ -324,9 +341,10 @@ Tags:
|
|
|
324
341
|
- `interface CarrierAdapter`
|
|
325
342
|
- `interface CarrierEvent`
|
|
326
343
|
- `interface ConsentConfig`
|
|
344
|
+
- `interface CouponRef`
|
|
327
345
|
- `type Currency = 'PLN'`
|
|
328
346
|
- `defineShop(config: ShopConfig): ResolvedShopConfig`
|
|
329
|
-
- `type GeowidgetConfigPreset =
|
|
347
|
+
- `type GeowidgetConfigPreset = 'parcelcollect' | 'parcelsend' | 'parcelcollect247' | string`
|
|
330
348
|
- `type I18nText = { [lang: string]: string; }`
|
|
331
349
|
- `inpostAdapter(opts: InpostAdapterOptions): CarrierAdapter`
|
|
332
350
|
- `interface InpostAdapterOptions`
|
|
@@ -339,6 +357,8 @@ Tags:
|
|
|
339
357
|
- `interface PaymentCreateContext`
|
|
340
358
|
- `interface PaymentCreateResult`
|
|
341
359
|
- `interface PaymentEvent`
|
|
360
|
+
- `interface PaymentRefundInput`
|
|
361
|
+
- `interface PaymentRefundResult`
|
|
342
362
|
- `payuAdapter(opts: PayuAdapterOptions): PaymentAdapter`
|
|
343
363
|
- `interface PayuAdapterOptions`
|
|
344
364
|
- `interface ResolvedShopConfig`
|
|
@@ -347,6 +367,8 @@ Tags:
|
|
|
347
367
|
- `interface ShipmentLabel`
|
|
348
368
|
- `interface ShopConfig`
|
|
349
369
|
- `interface ShopFeatures`
|
|
370
|
+
- `stripeAdapter(opts: StripeAdapterOptions): PaymentAdapter` — Stripe payment adapter (Checkout Session flow).
|
|
371
|
+
- `interface StripeAdapterOptions`
|
|
350
372
|
|
|
351
373
|
### `includio-cms/shop/client`
|
|
352
374
|
|
|
@@ -357,7 +379,7 @@ Tags:
|
|
|
357
379
|
- `interface CheckoutResult`
|
|
358
380
|
- `createOrderState(opts: CreateOrderStateOptions): OrderState`
|
|
359
381
|
- `interface CreateOrderStateOptions`
|
|
360
|
-
- `createShopClient(options: ShopClientOptions = {}): ShopClient`
|
|
382
|
+
- `createShopClient(options: ShopClientOptions = {}): ShopClient` — Create a typed, isomorphic shop SDK client.
|
|
361
383
|
- `const DEFAULT_LABELS_PL: OrderStatusLabels`
|
|
362
384
|
- `const InpostPicker: LegacyComponentType`
|
|
363
385
|
- `interface OrderDetailResponse`
|
|
@@ -368,13 +390,14 @@ Tags:
|
|
|
368
390
|
- `interface RefreshPaymentResult`
|
|
369
391
|
- `interface RetryPaymentResult`
|
|
370
392
|
- `interface ShippingMethodPublic`
|
|
371
|
-
- `interface ShopClient`
|
|
372
|
-
- `interface ShopClientOptions`
|
|
393
|
+
- `interface ShopClient` — Headless shop SDK surface returned by `createShopClient()`.
|
|
394
|
+
- `interface ShopClientOptions` — Options for `createShopClient()`.
|
|
373
395
|
|
|
374
396
|
### `includio-cms/shop/http`
|
|
375
397
|
|
|
376
398
|
- `createCarrierConfigHandler(): { GET: RequestHandler }` — Public endpoint that returns front-end facing carrier widget descriptor for
|
|
377
399
|
- `createCarrierWebhookHandler(): { POST: RequestHandler }` — Public webhook receiver for carrier events (e.g. ShipX `shipment_status_changed`).
|
|
400
|
+
- `createCartCouponHandler(): { POST: RequestHandler; DELETE: RequestHandler }` — Cart coupon endpoints — POST applies a code, DELETE removes it.
|
|
378
401
|
- `createCartHandler(): { GET: RequestHandler; POST: RequestHandler; PATCH: RequestHandler; DELETE: R...`
|
|
379
402
|
- `createCheckoutHandler(): { POST: RequestHandler }`
|
|
380
403
|
- `createOrderHandler(): { GET: RequestHandler }`
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,101 @@
|
|
|
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.25.0 — 2026-05-06
|
|
7
|
+
|
|
8
|
+
Faza 11.5 — Shop polish. Stripe payment adapter (Checkout Session flow z native webhook signature verify), refund infrastructure (full + partial, cross-adapter Stripe + PayU), webhook idempotency table (`shop_webhook_events` z UNIQUE(provider, event_id)), coupons (% / fixed PLN, expiresAt, maxUses, minOrderAmount, OCC race-safe redemption), low-stock email alert (per-product `lowStockThreshold`), order export CSV w admin, default email templates rozszerzone o `refunded` status, SDK `applyCoupon`/`removeCoupon` + JSDoc body + `@public` tagi, pełna dokumentacja shopa (DOCS.md + adapter guides + cURL examples + Stripe/Coupons/Refund pages). Shop = `@public` w v1.0.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `stripeAdapter()` — Stripe Checkout Session flow. `createPayment()` → `stripe.checkout.sessions.create` (zwraca redirect URL); `handleWebhook()` → natywny `stripe.webhooks.constructEvent` (sig + timestamp skew); `getStatus()` → `checkout.sessions.retrieve` poll fallback dla lost webhooks; `refund()` → resolve session → payment_intent → `stripe.refunds.create({ amount? })`. Stripe SDK = optional peer (lazy import + caching). Status mapping: `complete + paid → paid`, `expired → paymentRejected`. Eksport: `stripeAdapter`, `StripeAdapterOptions`.
|
|
12
|
+
- **Refund infrastructure** (cross-adapter, full + partial). `PaymentAdapter.refund?(input)` opcjonalna metoda interfejsu (`PaymentRefundInput` / `PaymentRefundResult`). Server: `refundOrder({ orderId, amount?, reason?, createdBy })` w `src/lib/shop/server/refund.ts` — walidacja eligibility (status `paid|preparing|sent|done`, `paymentProviderRef`, adapter wsparcie, `amount <= remaining`), insert pending row, call adapter, update do `succeeded`/`failed`, full refund → `updateOrderStatus(refunded)`. PayU `client.refundOrder()` + `payuAdapter().refund()`. Stripe refund built-in. DB: nowa tabela `shop_refunds` (orderId FK, paymentId FK?, provider, providerRef, amount cents, currency, reason, status, createdBy, createdAt). `getRefundedAmount()` + `listRefunds()` helpers.
|
|
13
|
+
- **Webhook idempotency table** (`shop_webhook_events`). UNIQUE(provider, event_id) — duplikaty short-circuit z 200 + `replay: true` przed mutacją state. PayU `notify_id` (lub fallback `${orderId}:${status}`), Stripe natywny `event.id`. `reserveWebhookEvent()` + `markWebhookEventProcessed()` w `src/lib/shop/http/webhook-idempotency.ts`. Stara terminal-status check zachowana jako fallback dla adapterów bez `eventId`.
|
|
14
|
+
- **Coupons** — code-based discounts. DB: `shop_coupons` (code UNIQUE uppercase, type `percent|fixed`, value numeric 20,6, minOrderAmount cents, maxUses, usedCount, expiresAt, isActive) + `shop_coupon_redemptions` (couponId FK, orderId FK UNIQUE, discountAmount, redeemedAt). Server: `validateCoupon()` (feature flag + lookup + isActive + expiresAt + maxUses + minOrderAmount), `reserveCouponSlot()` z OCC `WHERE max_uses IS NULL OR used_count < max_uses`, `recordCouponRedemption()` `onConflictDoNothing(orderId)`, `releaseCouponSlot()`. Pricing: discount przed VAT, uniform per-line factor (`calculateCouponDiscountNet()` + line-level factor w `cart-hydrate`). REST: `POST/DELETE /api/shop/cart/coupon` (`createCartCouponHandler()`), separate cookie `aria_shop_coupon` (7d TTL, `[A-Z0-9_-]{1,64}`). SDK: `cart.applyCoupon(code)` / `cart.removeCoupon()`. Admin UI: lista kodów + new/edit form, walidacja kodu (uppercase, alfanumeryczny). Feature flag: `defineShop({ features: { coupons: true } })`.
|
|
15
|
+
- **Low-stock email alert.** Per-produkt `lowStockThreshold: int?` w `shop_products`. Hook po `updateOrderStatus(paid)` decrement: jeśli `stock <= threshold` AND `stock + qty > threshold` (boundary cross), `sendLowStockEmail()` (PL) → `shop.adminEmail`. Brak `adminEmail` lub brak email adaptera = no-op.
|
|
16
|
+
- **Order export CSV** (admin). `exportOrdersCsv(filters)` query w `shop.remote.ts` — RFC 4180 escape (commas, quotes, newlines), BOM dla Excel UTF-8. Button "Eksport CSV" na orders list page (honors current `status` + `email` filters). Kolumny: number, status, customerEmail, customerName, totalGross, currency, shippingGross, paymentMethod, createdAt.
|
|
17
|
+
- **Default email templates** — `OrderStatus` += `refunded` (terminal w webhook-logic, propagacja: `STATUS_SUBJECTS`/`STATUS_INTRO` PL/EN, customer-facing labels, admin Zod schema, admin detail page styles). Plus low-stock template dla admina.
|
|
18
|
+
- **SDK polish** — `createShopClient()` + `ShopClient` + `ShopClientOptions` z pełnym JSDoc body (description + @param + @returns + @example). Per-method docs dla `cart.{get,add,update,remove,clear,applyCoupon,removeCoupon}`, `shipping.list`, `checkout.submit`, `orders.{get,refreshPayment,retryPayment}`. Wszystkie eksporty `@public`.
|
|
19
|
+
- **Dokumentacja** — pełna sekcja Shop w `DOCS.md` (overview rozszerzony o Coupons/Refunds/Stripe/Low-stock/CSV export, sekcja "cURL examples" + "Error codes"), 5 nowych stron: `/docs/shop/{stripe,coupons,refund,payment-adapter,carrier-adapter}`. `payment-adapter` guide pokazuje pełen interfejs + lazy peer pattern + signature verify + idempotency via eventId + lost-webhook recovery + refund. Update `nav.ts`.
|
|
20
|
+
- **Admin UI**. Order detail: sidebar "Zwroty" (lista refund rows + aggregate refunded/remaining + `RefundDialog` modal z full/partial mode + reason). Orders list: "Eksport CSV" button. Coupons CRUD: `/admin/shop/coupons` lista + `/new` + `/[id]` edit (validacja code uppercase + alfanumeryczny). Nav-shop link. Refunded status w STATUS_STYLES.
|
|
21
|
+
|
|
22
|
+
### Breaking
|
|
23
|
+
- `OrderStatus` typ rozszerzony o `"refunded"` (terminal status). Custom UI/email logic używające `Record<OrderStatus, …>` musi dodać entry. Zaktualizowane in-tree: email templates, `svelte/labels.ts` `DEFAULT_LABELS_PL`, admin detail page, admin Zod schema, webhook-logic terminal set.
|
|
24
|
+
- `PaymentAdapter` interface += optional `refund?(input)` method i optional `eventId?` / `eventType?` fields w `PaymentEvent`. Custom adaptery działają bez zmian (`refund` opcjonalne; brak `eventId` skutkuje fallback do terminal-status idempotency).
|
|
25
|
+
- `CartSnapshot` rozszerzony o `subtotalNet`/`subtotalGross`/`subtotalVat` + opcjonalny `coupon: AppliedCoupon`. `totalNet`/`totalGross` teraz reprezentują wartość PO odjęciu discountu. Storefronty czytające tylko `total*` działają bez zmian dla cartów bez coupons; kod renderujący detal pre-discount musi czytać `subtotal*`.
|
|
26
|
+
- `ShopFeatures` += opcjonalny `coupons?: boolean` (default `false`). `ShopConfig` += opcjonalny `adminEmail?: string` (default brak — low-stock alert no-op). Backward compatible defaults.
|
|
27
|
+
|
|
28
|
+
### Migration
|
|
29
|
+
|
|
30
|
+
```sql
|
|
31
|
+
-- 0.25.0 — Shop polish DB additions
|
|
32
|
+
-- Run via drizzle-kit push or apply manually before deploying 0.25.0.
|
|
33
|
+
|
|
34
|
+
-- Refunds — one row per refund attempt (succeeded / pending / failed)
|
|
35
|
+
CREATE TABLE IF NOT EXISTS shop_refunds (
|
|
36
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
37
|
+
order_id uuid NOT NULL REFERENCES shop_orders(id) ON DELETE CASCADE,
|
|
38
|
+
payment_id uuid REFERENCES shop_payments(id) ON DELETE SET NULL,
|
|
39
|
+
provider text NOT NULL,
|
|
40
|
+
provider_ref text,
|
|
41
|
+
amount integer NOT NULL,
|
|
42
|
+
currency text NOT NULL,
|
|
43
|
+
reason text,
|
|
44
|
+
status text NOT NULL DEFAULT 'pending',
|
|
45
|
+
created_by text,
|
|
46
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
47
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
-- Webhook idempotency log — UNIQUE(provider, event_id) prevents replay
|
|
51
|
+
CREATE TABLE IF NOT EXISTS shop_webhook_events (
|
|
52
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
53
|
+
provider text NOT NULL,
|
|
54
|
+
event_id text NOT NULL,
|
|
55
|
+
event_type text,
|
|
56
|
+
order_id uuid REFERENCES shop_orders(id) ON DELETE SET NULL,
|
|
57
|
+
order_number text,
|
|
58
|
+
payload_hash text,
|
|
59
|
+
raw jsonb,
|
|
60
|
+
received_at timestamptz NOT NULL DEFAULT now(),
|
|
61
|
+
processed_at timestamptz
|
|
62
|
+
);
|
|
63
|
+
CREATE UNIQUE INDEX IF NOT EXISTS shop_webhook_events_provider_event_unique
|
|
64
|
+
ON shop_webhook_events (provider, event_id);
|
|
65
|
+
|
|
66
|
+
-- Coupons
|
|
67
|
+
CREATE TABLE IF NOT EXISTS shop_coupons (
|
|
68
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
69
|
+
code text NOT NULL,
|
|
70
|
+
type text NOT NULL,
|
|
71
|
+
value numeric(20,6) NOT NULL,
|
|
72
|
+
min_order_amount integer,
|
|
73
|
+
max_uses integer,
|
|
74
|
+
used_count integer NOT NULL DEFAULT 0,
|
|
75
|
+
expires_at timestamptz,
|
|
76
|
+
is_active boolean NOT NULL DEFAULT true,
|
|
77
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
78
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
79
|
+
);
|
|
80
|
+
CREATE UNIQUE INDEX IF NOT EXISTS shop_coupons_code_unique ON shop_coupons (code);
|
|
81
|
+
|
|
82
|
+
-- Coupon redemptions — UNIQUE(order_id) prevents double-redeem on retry
|
|
83
|
+
CREATE TABLE IF NOT EXISTS shop_coupon_redemptions (
|
|
84
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
85
|
+
coupon_id uuid NOT NULL REFERENCES shop_coupons(id) ON DELETE CASCADE,
|
|
86
|
+
order_id uuid NOT NULL REFERENCES shop_orders(id) ON DELETE CASCADE,
|
|
87
|
+
discount_amount integer NOT NULL,
|
|
88
|
+
redeemed_at timestamptz NOT NULL DEFAULT now()
|
|
89
|
+
);
|
|
90
|
+
CREATE UNIQUE INDEX IF NOT EXISTS shop_coupon_redemptions_order_unique
|
|
91
|
+
ON shop_coupon_redemptions (order_id);
|
|
92
|
+
|
|
93
|
+
-- Low-stock alert column
|
|
94
|
+
ALTER TABLE shop_products ADD COLUMN IF NOT EXISTS low_stock_threshold integer;
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Notes
|
|
98
|
+
|
|
99
|
+
Migracja czysto additive — istniejące zamówienia pozostają nietknięte, refunded jest nową gałęzią graph statusów. Stripe SDK trzeba zainstalować osobno (`pnpm add stripe`) jeśli używa się `stripeAdapter()` — `optionalDependencies` + `peerDependenciesMeta`. Coupons opt-in via `features: { coupons: true }`. Low-stock email wymaga `shop.adminEmail` w `defineShop` + skonfigurowanego email adaptera. Webhook idempotency table jest backwards-compatible: adaptery które nie surface eventId nadal działają poprzez terminal-status fallback. Test coverage 1053/1053 unit tests zielone (37 nowych: Stripe sig verify 4, getStatus 1, refund 3, payload 6, status-map 10, pricing-coupon 5, coupon-cookie 8). DoD: gap analysis vs Woo/Presta zatwierdzona, MUST-v1 (Stripe + Refund + Idempotency + Coupons) wdrożone, NICE-v1 (Low-stock + CSV export) wdrożone, dokumentacja shopa pełna.
|
|
100
|
+
|
|
6
101
|
## 0.24.0 — 2026-04-30
|
|
7
102
|
|
|
8
103
|
Faza 11 — Performance Pass. Eliminacja N+1 w render path: `getImageStylesBatch` (jeden SELECT zamiast N×getImageStyle dla styles + srcset wariantów) + `_getRawEntries` z batchowanym `getEntryVersions` (jeden call zamiast per-entry). Sharp srcset pipeline odrefactorowany — Promise.all wokół per-style queries usunięty (jeden batch + sequential Sharp generation z try/catch dla missing entries). Background maintenance lock potwierdzony testami (concurrent invocation skipped). Bench infra: nowy projekt vitest `bench` + skrypt `pnpm bench` + 2 representative benchmarks (image styles + raw entries).
|
package/DOCS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Includio CMS Documentation (v0.24.
|
|
1
|
+
# Includio CMS Documentation (v0.24.1)
|
|
2
2
|
|
|
3
3
|
> This file is auto-generated from the docs site. For the latest version, update the package.
|
|
4
4
|
|
|
@@ -4987,18 +4987,22 @@ For error handling patterns (`CmsError`, `instanceof`-based branching), see [Ent
|
|
|
4987
4987
|
|
|
4988
4988
|
Headless e-commerce module. Optional — activate by adding `shop: defineShop({...})` to your CMS config.
|
|
4989
4989
|
|
|
4990
|
-
> Shop is
|
|
4990
|
+
> Shop is `@public` from 0.25.0 — covered by the v1 stability promise. Breaking changes only at major bumps.
|
|
4991
4991
|
|
|
4992
4992
|
## What you get
|
|
4993
4993
|
|
|
4994
4994
|
- **Products as entries** — add `{ type: 'shop', slug: 'shop' }` to any collection; its entries become purchasable.
|
|
4995
4995
|
- **Cart** — signed cookie, headless SDK (`createShopClient()`), `POST/PATCH/DELETE /api/shop/cart`.
|
|
4996
|
-
- **
|
|
4997
|
-
- **
|
|
4996
|
+
- **Coupons** — code-based discounts (% / fixed PLN), expiry, max-uses, min-order. Apply via `cart.applyCoupon('CODE')`. See [Coupons](/docs/shop/coupons).
|
|
4997
|
+
- **Checkout + orders** — `POST /api/shop/checkout` creates an order with consent validation, stock reservation (30-min TTL), coupon redemption (race-safe), and status emails.
|
|
4998
|
+
- **Payment adapters** — `manualAdapter()` (bank transfer / COD), `payuAdapter()`, and `stripeAdapter()` ship built-in. See [Stripe](/docs/shop/stripe) and [Writing your own payment adapter](/docs/shop/payment-adapter).
|
|
4999
|
+
- **Refunds** — full and partial, cross-adapter (Stripe + PayU). Admin button on order detail. See [Refunds](/docs/shop/refund).
|
|
4998
5000
|
- **Carrier adapters** — `inpostAdapter()` ships built-in (Geowidget v5 picker + ShipX shipments + webhook). Plug other carriers by implementing `CarrierAdapter`.
|
|
4999
|
-
- **Webhook infrastructure** — `createPaymentWebhookHandler()` dispatches provider callbacks, verifies signatures,
|
|
5001
|
+
- **Webhook infrastructure** — `createPaymentWebhookHandler()` dispatches provider callbacks, verifies signatures, persists `(provider, event_id)` for proper idempotency.
|
|
5000
5002
|
- **Secure order view** — per-order `accessToken` + cookie fallback + `GET /api/shop/orders/[number]` token-gated API.
|
|
5001
5003
|
- **Shipping ↔ payment compatibility** — restrict payment methods per shipping (e.g. no COD for paczkomat).
|
|
5004
|
+
- **Low-stock alert** — set `lowStockThreshold` per product; admin gets an email when stock crosses the boundary.
|
|
5005
|
+
- **Order export** — admin "Export CSV" button on the orders list (honors current filters).
|
|
5002
5006
|
|
|
5003
5007
|
## Config
|
|
5004
5008
|
|
|
@@ -5045,6 +5049,7 @@ Scaffold provisions these endpoints (when shop is enabled):
|
|
|
5045
5049
|
| Path | Handler | Purpose |
|
|
5046
5050
|
|------|---------|---------|
|
|
5047
5051
|
| `POST /api/shop/cart` | `createCartHandler()` | Cart CRUD |
|
|
5052
|
+
| `POST/DELETE /api/shop/cart/coupon` | `createCartCouponHandler()` | Apply / remove coupon |
|
|
5048
5053
|
| `GET /api/shop/shipping-methods` | `createShippingMethodsHandler()` | List active shipping |
|
|
5049
5054
|
| `POST /api/shop/checkout` | `createCheckoutHandler()` | Create order + initiate payment |
|
|
5050
5055
|
| `GET /api/shop/orders/[number]` | `createOrderHandler()` | Token-gated order view |
|
|
@@ -5055,8 +5060,78 @@ Scaffold provisions these endpoints (when shop is enabled):
|
|
|
5055
5060
|
| `POST /api/shop/carriers/[id]/webhook` | `createCarrierWebhookHandler()` | Carrier event receiver |
|
|
5056
5061
|
| `GET /api/shop/admin/orders/[id]/label` | `createShipmentLabelHandler()` | Admin shipping-label PDF proxy |
|
|
5057
5062
|
|
|
5063
|
+
## cURL examples
|
|
5064
|
+
|
|
5065
|
+
Get cart (cookie-based):
|
|
5066
|
+
|
|
5067
|
+
```sh
|
|
5068
|
+
curl -i 'https://example.com/api/shop/cart' --cookie cookies.txt --cookie-jar cookies.txt
|
|
5069
|
+
```
|
|
5070
|
+
|
|
5071
|
+
Add a variant:
|
|
5072
|
+
|
|
5073
|
+
```sh
|
|
5074
|
+
curl -X POST 'https://example.com/api/shop/cart' \
|
|
5075
|
+
-H 'Content-Type: application/json' \
|
|
5076
|
+
-d '{"variantId":"v_abc","qty":2}' \
|
|
5077
|
+
--cookie cookies.txt --cookie-jar cookies.txt
|
|
5078
|
+
```
|
|
5079
|
+
|
|
5080
|
+
Apply a coupon:
|
|
5081
|
+
|
|
5082
|
+
```sh
|
|
5083
|
+
curl -X POST 'https://example.com/api/shop/cart/coupon' \
|
|
5084
|
+
-H 'Content-Type: application/json' \
|
|
5085
|
+
-d '{"code":"ARIA10"}' \
|
|
5086
|
+
--cookie cookies.txt --cookie-jar cookies.txt
|
|
5087
|
+
```
|
|
5088
|
+
|
|
5089
|
+
Submit checkout:
|
|
5090
|
+
|
|
5091
|
+
```sh
|
|
5092
|
+
curl -X POST 'https://example.com/api/shop/checkout' \
|
|
5093
|
+
-H 'Content-Type: application/json' \
|
|
5094
|
+
-d '{
|
|
5095
|
+
"customerEmail": "buyer@example.com",
|
|
5096
|
+
"shippingMethodId": "ship_xyz",
|
|
5097
|
+
"paymentMethod": "stripe",
|
|
5098
|
+
"consents": [{"id": "terms", "accepted": true}]
|
|
5099
|
+
}' \
|
|
5100
|
+
--cookie cookies.txt
|
|
5101
|
+
# → { "orderNumber": "ARI-X1Y2Z3", "redirectUrl": "https://checkout.stripe.com/...", "requiresPaymentRedirect": true }
|
|
5102
|
+
```
|
|
5103
|
+
|
|
5104
|
+
Fetch order (with access token):
|
|
5105
|
+
|
|
5106
|
+
```sh
|
|
5107
|
+
curl 'https://example.com/api/shop/orders/ARI-X1Y2Z3?token=<accessToken>'
|
|
5108
|
+
```
|
|
5109
|
+
|
|
5110
|
+
## Error codes
|
|
5111
|
+
|
|
5112
|
+
Cart endpoints return `400` with `{ "error": "<code>" }` for known failures:
|
|
5113
|
+
|
|
5114
|
+
| Code | Meaning |
|
|
5115
|
+
|------|---------|
|
|
5116
|
+
| `code required` | `POST /cart/coupon` body lacks `code` |
|
|
5117
|
+
| `invalid_code` | Coupon code format invalid (uppercase / 1–64 chars / `[A-Z0-9_-]`) |
|
|
5118
|
+
| `not_found` | Coupon code does not exist |
|
|
5119
|
+
| `inactive` | Coupon set to inactive |
|
|
5120
|
+
| `expired` | `expiresAt` in the past |
|
|
5121
|
+
| `exhausted` | `usedCount >= maxUses` |
|
|
5122
|
+
| `min_order_not_met` | Cart subtotal below `minOrderAmount` |
|
|
5123
|
+
| `feature_disabled` | `features.coupons` is `false` in `defineShop` |
|
|
5124
|
+
| `empty_cart` | No items to apply discount to |
|
|
5125
|
+
|
|
5126
|
+
Webhook endpoints return `200` for replay/already-processed events (`{"replay": true}`), `400` for signature/payload failures, `429` rate-limit, `500` for provider/server errors (provider should retry).
|
|
5127
|
+
|
|
5058
5128
|
## See also
|
|
5059
5129
|
|
|
5130
|
+
- [Stripe](/docs/shop/stripe) — Checkout Session adapter + webhook + refund
|
|
5131
|
+
- [Coupons](/docs/shop/coupons) — code-based discounts, % / fixed, expiry, max-uses
|
|
5132
|
+
- [Refunds](/docs/shop/refund) — full + partial, cross-adapter
|
|
5133
|
+
- [Writing your own payment adapter](/docs/shop/payment-adapter)
|
|
5134
|
+
- [Writing your own carrier adapter](/docs/shop/carrier-adapter)
|
|
5060
5135
|
- [Order view](/docs/shop/order-view) — generic `<OrderStatus>` component + headless helper
|
|
5061
5136
|
- [Retry payment](/docs/shop/retry-payment) — lifecycle + endpoint
|
|
5062
5137
|
- [InPost carrier](/docs/shop/inpost) — Geowidget v5 + ShipX shipment + webhook
|
package/ROADMAP.md
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
- [x] `[breaking]` `[P0]` Faza 9 — DX & config validation (0.22.0): `defineConfig` Zod strict z field path + hint, `CmsError` + `ConfigValidationError` z code/context, resolver throw sites zmigrowane, CLI `--help` per subcommand + `--version`, `.env.example` rozszerzony o `INCLUDIO_*`, README rebuild (TOC + system req + 5-min quickstart), JSDoc body (opis + @param + @returns + @example) na każdym `@public` <!-- files: src/lib/core/errors.ts, src/lib/types/cms.schema.ts, src/lib/sveltekit/config.ts, src/lib/cli/index.ts, README.md, .env.example, src/lib/updates/0.22.0/ -->
|
|
19
19
|
- [x] `[chore]` `[P1]` Faza 10 — Documentation Pass (0.23.0): DOCS.md uzupełnione (number/boolean/date edge cases, REST cURL + error codes, Entries error handling + transactions, Admin UI a11y, Adapter Contracts), nowe sekcje (Stability Promise, Security Model), Migration Guide v0.x → v1.0 master cheatsheet (0.16-0.22), ROADMAP cleanup (pre-v1.0 → ROADMAP-ARCHIVE.md) <!-- files: src/routes/docs/{stability-promise,security}/+page.svx, src/routes/docs/migration/+page.svx, src/routes/docs/_config/nav.ts, scripts/compile-docs.ts, ROADMAP-ARCHIVE.md, src/lib/updates/0.23.0/ -->
|
|
20
20
|
- [x] `[chore]` `[P1]` Faza 11 — Performance Pass (0.24.0): N+1 elimination (`getImageStyle` batch IN query, `getRawEntries` batch entry versions), Sharp srcset `Promise.allSettled` (one timeout doesn't kill batch), background maintenance in-process lock verified + tested, bench infra (`pnpm bench` + representative benchmarks), lazy adapter imports verified <!-- files: src/lib/core/server/media/styles/operations/, src/lib/core/server/fields/resolveImageFields.ts, src/lib/core/server/entries/operations/get.ts, src/lib/db-postgres/index.ts, src/lib/core/server/media/operations/backgroundMaintenance.spec.ts, src/lib/updates/0.24.0/ -->
|
|
21
|
+
- [x] `[feature]` `[P0]` Faza 11.5 — Shop polish (0.25.0): Stripe adapter (Checkout Session, sig verify, idempotency), refund infra (full + partial cross-adapter), webhook idempotency table (`shop_webhook_events`), coupons (% / fixed, expiresAt, maxUses, minOrder), low-stock email alert, order export CSV, default email templates (PL/EN), SDK JSDoc + `@public`, pełna dokumentacja shopa <!-- files: src/lib/shop/adapters/stripe/, src/lib/shop/server/{refund,coupons}.ts, src/lib/db-postgres/schema/shop/{refunds,webhookEvents,coupons,couponRedemptions}.ts, src/lib/admin/client/shop/, src/lib/updates/0.25.0/ -->
|
|
21
22
|
- [ ] `[fix]` `[P1]` Select field — `defaultValue` propagacja do zod schema (full repro: `ideas/post-v1/select-field-defaultvalue-bug.md`); fix planowany w Fazie 12 (RC)
|
|
22
23
|
|
|
23
24
|
## v1.x — Post-v1.0 deferred
|
|
@@ -18,5 +18,8 @@ export { default as ShippingMethodNewPage } from './shop/shipping-method-new-pag
|
|
|
18
18
|
export { default as ShippingMethodEditPage } from './shop/shipping-method-edit-page.svelte';
|
|
19
19
|
export { default as ShopOrdersListPage } from './shop/shop-orders-list-page.svelte';
|
|
20
20
|
export { default as ShopOrderDetailPage } from './shop/shop-order-detail-page.svelte';
|
|
21
|
+
export { default as ShopCouponsListPage } from './shop/coupons-list-page.svelte';
|
|
22
|
+
export { default as ShopCouponNewPage } from './shop/coupon-new-page.svelte';
|
|
23
|
+
export { default as ShopCouponEditPage } from './shop/coupon-edit-page.svelte';
|
|
21
24
|
export * from '../helpers/index.js';
|
|
22
25
|
export * from '../ui/index.js';
|
|
@@ -18,6 +18,9 @@ export { default as ShippingMethodNewPage } from './shop/shipping-method-new-pag
|
|
|
18
18
|
export { default as ShippingMethodEditPage } from './shop/shipping-method-edit-page.svelte';
|
|
19
19
|
export { default as ShopOrdersListPage } from './shop/shop-orders-list-page.svelte';
|
|
20
20
|
export { default as ShopOrderDetailPage } from './shop/shop-order-detail-page.svelte';
|
|
21
|
+
export { default as ShopCouponsListPage } from './shop/coupons-list-page.svelte';
|
|
22
|
+
export { default as ShopCouponNewPage } from './shop/coupon-new-page.svelte';
|
|
23
|
+
export { default as ShopCouponEditPage } from './shop/coupon-edit-page.svelte';
|
|
21
24
|
// Folded from `./admin/helpers` (dropped as separate export in 0.20.0)
|
|
22
25
|
export * from '../helpers/index.js';
|
|
23
26
|
// Folded from `./admin/ui` (dropped as separate export in 0.20.0)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { goto } from '$app/navigation';
|
|
3
|
+
import { page } from '$app/state';
|
|
4
|
+
import { getRemotes } from '../../../sveltekit/index.js';
|
|
5
|
+
import CouponForm from './coupon-form.svelte';
|
|
6
|
+
|
|
7
|
+
const remotes = getRemotes();
|
|
8
|
+
const id = $derived(page.params.id ?? '');
|
|
9
|
+
const query = $derived(remotes.getCouponAdmin(id));
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<div class="mx-auto max-w-2xl space-y-6 p-6">
|
|
13
|
+
<header>
|
|
14
|
+
<h1 class="text-2xl font-extrabold tracking-tight">Edytuj kod rabatowy</h1>
|
|
15
|
+
<p class="text-muted-foreground text-sm">
|
|
16
|
+
<a href="/admin/shop/coupons" class="hover:underline">← Wróć do listy</a>
|
|
17
|
+
</p>
|
|
18
|
+
</header>
|
|
19
|
+
|
|
20
|
+
{#if !query.ready}
|
|
21
|
+
<p class="text-muted-foreground">Ładowanie…</p>
|
|
22
|
+
{:else if !query.current}
|
|
23
|
+
<p>Kod nie został znaleziony.</p>
|
|
24
|
+
{:else}
|
|
25
|
+
{@const c = query.current}
|
|
26
|
+
<CouponForm
|
|
27
|
+
initial={{
|
|
28
|
+
code: c.code,
|
|
29
|
+
type: c.type,
|
|
30
|
+
value: Number(c.value),
|
|
31
|
+
minOrderAmount: c.minOrderAmount,
|
|
32
|
+
maxUses: c.maxUses,
|
|
33
|
+
expiresAt: c.expiresAt instanceof Date ? c.expiresAt.toISOString() : c.expiresAt,
|
|
34
|
+
isActive: c.isActive
|
|
35
|
+
}}
|
|
36
|
+
submitLabel="Zapisz zmiany"
|
|
37
|
+
onSubmit={async (input) => {
|
|
38
|
+
await remotes.updateCouponCmd({ id, input });
|
|
39
|
+
await goto('/admin/shop/coupons');
|
|
40
|
+
}}
|
|
41
|
+
onCancel={() => goto('/admin/shop/coupons')}
|
|
42
|
+
/>
|
|
43
|
+
{/if}
|
|
44
|
+
</div>
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Input } from '../../../components/ui/input/index.js';
|
|
3
|
+
import { Button } from '../../../components/ui/button/index.js';
|
|
4
|
+
import Label from '../../../components/ui/label/label.svelte';
|
|
5
|
+
|
|
6
|
+
type CouponInput = {
|
|
7
|
+
code: string;
|
|
8
|
+
type: 'percent' | 'fixed';
|
|
9
|
+
value: number;
|
|
10
|
+
minOrderAmount: number | null;
|
|
11
|
+
maxUses: number | null;
|
|
12
|
+
expiresAt: string | null;
|
|
13
|
+
isActive: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type Props = {
|
|
17
|
+
initial?: Partial<CouponInput>;
|
|
18
|
+
submitLabel?: string;
|
|
19
|
+
onSubmit: (input: CouponInput) => Promise<void>;
|
|
20
|
+
onCancel?: () => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let { initial, submitLabel = 'Zapisz', onSubmit, onCancel }: Props = $props();
|
|
24
|
+
|
|
25
|
+
let code = $state(initial?.code ?? '');
|
|
26
|
+
let type = $state<'percent' | 'fixed'>(initial?.type ?? 'percent');
|
|
27
|
+
let value = $state(initial?.value != null ? String(initial.value) : '');
|
|
28
|
+
let minOrderAmountPln = $state(
|
|
29
|
+
initial?.minOrderAmount != null ? (initial.minOrderAmount / 100).toFixed(2) : ''
|
|
30
|
+
);
|
|
31
|
+
let maxUses = $state(initial?.maxUses != null ? String(initial.maxUses) : '');
|
|
32
|
+
let expiresAt = $state(initial?.expiresAt ? initial.expiresAt.slice(0, 10) : '');
|
|
33
|
+
let isActive = $state(initial?.isActive ?? true);
|
|
34
|
+
|
|
35
|
+
let submitting = $state(false);
|
|
36
|
+
let error = $state<string | null>(null);
|
|
37
|
+
|
|
38
|
+
async function handleSubmit(e: Event) {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
error = null;
|
|
41
|
+
const numValue = Number(value.replace(',', '.'));
|
|
42
|
+
if (!Number.isFinite(numValue) || numValue <= 0) {
|
|
43
|
+
error = 'Podaj wartość większą od zera.';
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (type === 'percent' && numValue > 100) {
|
|
47
|
+
error = 'Procent nie może przekraczać 100.';
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const minOrderCents = minOrderAmountPln
|
|
51
|
+
? Math.round(Number(minOrderAmountPln.replace(',', '.')) * 100)
|
|
52
|
+
: null;
|
|
53
|
+
if (minOrderCents != null && (!Number.isFinite(minOrderCents) || minOrderCents < 0)) {
|
|
54
|
+
error = 'Minimalna wartość zamówienia musi być nieujemna.';
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const maxUsesNum = maxUses ? Number(maxUses) : null;
|
|
58
|
+
if (maxUsesNum != null && (!Number.isInteger(maxUsesNum) || maxUsesNum <= 0)) {
|
|
59
|
+
error = 'Maksymalna liczba użyć musi być dodatnią liczbą całkowitą.';
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
submitting = true;
|
|
64
|
+
try {
|
|
65
|
+
await onSubmit({
|
|
66
|
+
code: code.trim().toUpperCase(),
|
|
67
|
+
type,
|
|
68
|
+
value: numValue,
|
|
69
|
+
minOrderAmount: minOrderCents,
|
|
70
|
+
maxUses: maxUsesNum,
|
|
71
|
+
expiresAt: expiresAt ? new Date(expiresAt).toISOString() : null,
|
|
72
|
+
isActive
|
|
73
|
+
});
|
|
74
|
+
} catch (err) {
|
|
75
|
+
error = err instanceof Error ? err.message : 'Nie udało się zapisać.';
|
|
76
|
+
submitting = false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<form onsubmit={handleSubmit} class="space-y-5">
|
|
82
|
+
<div class="space-y-2">
|
|
83
|
+
<Label for="coupon-code">Kod</Label>
|
|
84
|
+
<Input
|
|
85
|
+
id="coupon-code"
|
|
86
|
+
bind:value={code}
|
|
87
|
+
placeholder="np. ARIA10"
|
|
88
|
+
required
|
|
89
|
+
pattern="[A-Za-z0-9_-]+"
|
|
90
|
+
maxlength={64}
|
|
91
|
+
aria-describedby="coupon-code-hint"
|
|
92
|
+
/>
|
|
93
|
+
<p id="coupon-code-hint" class="text-muted-foreground text-xs">
|
|
94
|
+
Wielkość liter zostanie ujednolicona do dużych liter. Tylko litery, cyfry, „_”, „-”.
|
|
95
|
+
</p>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<fieldset class="space-y-2">
|
|
99
|
+
<legend class="mb-1 text-sm font-semibold">Typ rabatu</legend>
|
|
100
|
+
<label class="flex items-center gap-2 text-sm">
|
|
101
|
+
<input type="radio" name="coupon-type" value="percent" bind:group={type} />
|
|
102
|
+
<span>Procent (% od kwoty netto)</span>
|
|
103
|
+
</label>
|
|
104
|
+
<label class="flex items-center gap-2 text-sm">
|
|
105
|
+
<input type="radio" name="coupon-type" value="fixed" bind:group={type} />
|
|
106
|
+
<span>Kwota stała (PLN)</span>
|
|
107
|
+
</label>
|
|
108
|
+
</fieldset>
|
|
109
|
+
|
|
110
|
+
<div class="space-y-2">
|
|
111
|
+
<Label for="coupon-value">{type === 'percent' ? 'Procent (0–100)' : 'Kwota (PLN)'}</Label>
|
|
112
|
+
<Input
|
|
113
|
+
id="coupon-value"
|
|
114
|
+
type="text"
|
|
115
|
+
inputmode="decimal"
|
|
116
|
+
bind:value
|
|
117
|
+
placeholder={type === 'percent' ? 'np. 10' : 'np. 50,00'}
|
|
118
|
+
required
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div class="grid grid-cols-2 gap-4">
|
|
123
|
+
<div class="space-y-2">
|
|
124
|
+
<Label for="coupon-min-order">Min. wartość zamówienia (PLN)</Label>
|
|
125
|
+
<Input
|
|
126
|
+
id="coupon-min-order"
|
|
127
|
+
type="text"
|
|
128
|
+
inputmode="decimal"
|
|
129
|
+
bind:value={minOrderAmountPln}
|
|
130
|
+
placeholder="opcjonalnie"
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="space-y-2">
|
|
134
|
+
<Label for="coupon-max-uses">Maks. liczba użyć</Label>
|
|
135
|
+
<Input
|
|
136
|
+
id="coupon-max-uses"
|
|
137
|
+
type="number"
|
|
138
|
+
min="1"
|
|
139
|
+
step="1"
|
|
140
|
+
bind:value={maxUses}
|
|
141
|
+
placeholder="bez limitu"
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div class="space-y-2">
|
|
147
|
+
<Label for="coupon-expires-at">Data wygaśnięcia</Label>
|
|
148
|
+
<Input id="coupon-expires-at" type="date" bind:value={expiresAt} />
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<label class="flex items-center gap-2 text-sm">
|
|
152
|
+
<input type="checkbox" bind:checked={isActive} />
|
|
153
|
+
<span>Aktywny (dostępny przy checkoucie)</span>
|
|
154
|
+
</label>
|
|
155
|
+
|
|
156
|
+
{#if error}
|
|
157
|
+
<p class="text-destructive text-sm" role="alert">{error}</p>
|
|
158
|
+
{/if}
|
|
159
|
+
|
|
160
|
+
<div class="flex gap-2">
|
|
161
|
+
<Button type="submit" disabled={submitting}>
|
|
162
|
+
{submitting ? 'Zapisywanie…' : submitLabel}
|
|
163
|
+
</Button>
|
|
164
|
+
{#if onCancel}
|
|
165
|
+
<Button type="button" variant="outline" onclick={onCancel} disabled={submitting}>
|
|
166
|
+
Anuluj
|
|
167
|
+
</Button>
|
|
168
|
+
{/if}
|
|
169
|
+
</div>
|
|
170
|
+
</form>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type CouponInput = {
|
|
2
|
+
code: string;
|
|
3
|
+
type: 'percent' | 'fixed';
|
|
4
|
+
value: number;
|
|
5
|
+
minOrderAmount: number | null;
|
|
6
|
+
maxUses: number | null;
|
|
7
|
+
expiresAt: string | null;
|
|
8
|
+
isActive: boolean;
|
|
9
|
+
};
|
|
10
|
+
type Props = {
|
|
11
|
+
initial?: Partial<CouponInput>;
|
|
12
|
+
submitLabel?: string;
|
|
13
|
+
onSubmit: (input: CouponInput) => Promise<void>;
|
|
14
|
+
onCancel?: () => void;
|
|
15
|
+
};
|
|
16
|
+
declare const CouponForm: import("svelte").Component<Props, {}, "">;
|
|
17
|
+
type CouponForm = ReturnType<typeof CouponForm>;
|
|
18
|
+
export default CouponForm;
|