includio-cms 0.15.2 → 0.15.4

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 (77) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/DOCS.md +142 -3
  3. package/ROADMAP.md +13 -2
  4. package/dist/admin/client/shop/shipping-method-form.svelte +66 -1
  5. package/dist/admin/client/shop/shipping-method-form.svelte.d.ts +8 -0
  6. package/dist/admin/client/shop/shop-order-detail-page.svelte +101 -0
  7. package/dist/admin/remote/shop.remote.d.ts +44 -0
  8. package/dist/admin/remote/shop.remote.js +35 -0
  9. package/dist/cli/index.js +49 -4
  10. package/dist/cli/scaffold/admin.d.ts +9 -2
  11. package/dist/cli/scaffold/admin.js +91 -3
  12. package/dist/core/server/forms/submissions/operations/create.js +11 -5
  13. package/dist/db-postgres/schema/shop/order.d.ts +68 -0
  14. package/dist/db-postgres/schema/shop/order.js +4 -0
  15. package/dist/db-postgres/schema/shop/shippingMethod.d.ts +25 -0
  16. package/dist/db-postgres/schema/shop/shippingMethod.js +1 -0
  17. package/dist/paraglide/messages/_index.d.ts +3 -36
  18. package/dist/paraglide/messages/_index.js +3 -71
  19. package/dist/paraglide/messages/hello_world.d.ts +5 -0
  20. package/dist/paraglide/messages/hello_world.js +33 -0
  21. package/dist/paraglide/messages/login_hello.d.ts +16 -0
  22. package/dist/paraglide/messages/login_hello.js +34 -0
  23. package/dist/paraglide/messages/login_please_login.d.ts +16 -0
  24. package/dist/paraglide/messages/login_please_login.js +34 -0
  25. package/dist/shop/adapters/inpost/geowidget.d.ts +27 -0
  26. package/dist/shop/adapters/inpost/geowidget.js +31 -0
  27. package/dist/shop/adapters/inpost/index.d.ts +89 -0
  28. package/dist/shop/adapters/inpost/index.js +156 -0
  29. package/dist/shop/adapters/inpost/payload.d.ts +18 -0
  30. package/dist/shop/adapters/inpost/payload.js +85 -0
  31. package/dist/shop/adapters/inpost/points-api.d.ts +17 -0
  32. package/dist/shop/adapters/inpost/points-api.js +55 -0
  33. package/dist/shop/adapters/inpost/shipx-client.d.ts +56 -0
  34. package/dist/shop/adapters/inpost/shipx-client.js +95 -0
  35. package/dist/shop/adapters/inpost/status-map.d.ts +9 -0
  36. package/dist/shop/adapters/inpost/status-map.js +46 -0
  37. package/dist/shop/adapters/inpost/webhook.d.ts +16 -0
  38. package/dist/shop/adapters/inpost/webhook.js +55 -0
  39. package/dist/shop/client/index.d.ts +5 -0
  40. package/dist/shop/http/carrier-handler.d.ts +12 -0
  41. package/dist/shop/http/carrier-handler.js +45 -0
  42. package/dist/shop/http/carrier-webhook-handler.d.ts +13 -0
  43. package/dist/shop/http/carrier-webhook-handler.js +66 -0
  44. package/dist/shop/http/checkout-handler.js +23 -1
  45. package/dist/shop/http/index.d.ts +3 -0
  46. package/dist/shop/http/index.js +3 -0
  47. package/dist/shop/http/order-handler.js +14 -0
  48. package/dist/shop/http/shipment-label-handler.d.ts +10 -0
  49. package/dist/shop/http/shipment-label-handler.js +53 -0
  50. package/dist/shop/http/shipping-handler.js +3 -0
  51. package/dist/shop/index.d.ts +3 -1
  52. package/dist/shop/index.js +1 -0
  53. package/dist/shop/server/email.js +37 -0
  54. package/dist/shop/server/orders.d.ts +9 -0
  55. package/dist/shop/server/orders.js +48 -0
  56. package/dist/shop/server/shipments.d.ts +33 -0
  57. package/dist/shop/server/shipments.js +145 -0
  58. package/dist/shop/server/shipping.d.ts +2 -1
  59. package/dist/shop/server/shipping.js +9 -0
  60. package/dist/shop/svelte/InpostPicker.svelte +270 -0
  61. package/dist/shop/svelte/InpostPicker.svelte.d.ts +51 -0
  62. package/dist/shop/svelte/OrderStatus.svelte +53 -1
  63. package/dist/shop/svelte/index.d.ts +1 -0
  64. package/dist/shop/svelte/index.js +1 -0
  65. package/dist/shop/svelte/labels.d.ts +5 -0
  66. package/dist/shop/svelte/labels.js +6 -1
  67. package/dist/shop/types.d.ts +49 -1
  68. package/dist/updates/0.15.3/index.d.ts +2 -0
  69. package/dist/updates/0.15.3/index.js +19 -0
  70. package/dist/updates/0.15.4/index.d.ts +2 -0
  71. package/dist/updates/0.15.4/index.js +14 -0
  72. package/dist/updates/index.js +3 -1
  73. package/package.json +1 -1
  74. package/dist/paraglide/messages/en.d.ts +0 -5
  75. package/dist/paraglide/messages/en.js +0 -14
  76. package/dist/paraglide/messages/pl.d.ts +0 -5
  77. package/dist/paraglide/messages/pl.js +0 -14
package/CHANGELOG.md CHANGED
@@ -3,6 +3,46 @@
3
3
  All notable changes to includio-cms are documented here.
4
4
  Generated from `src/lib/updates/` — do not edit manually.
5
5
 
6
+ ## 0.15.4 — 2026-04-21
7
+
8
+ Forms: auto-scaffolded public submission endpoint + decoupled notification emails from submission success.
9
+
10
+ ### Added
11
+ - CLI scaffold (`pnpm includio scaffold admin`) now emits `src/routes/api/forms/[slug]/submit/+server.ts` — the public POST endpoint for form submissions. Previously each project had to hand-roll this file (the handler lives in the library, but SvelteKit does not load routes from `node_modules`). New projects get it out-of-the-box; existing projects can run `includio scaffold admin` to generate it without overwriting other admin files.
12
+ - Added `ideas/health-check-module.md` — proposal for built-in `/api/health` + `/api/health/ready` endpoints, opt-in SMTP verify, and a ręczny "Test SMTP connection / Send test mail" section in the admin maintenance page. Context: diagnosing a silent SMTP misconfiguration in a live project required grepping through library internals; a one-click diagnostics panel would have surfaced the empty `EMAIL_HOST` immediately.
13
+
14
+ ### Fixed
15
+ - `createFormSubmission()` now keeps the notification-email call in a separate `try/catch` from the DB write. Before: if SMTP was misconfigured (empty `EMAIL_HOST` locally, unreachable relay, etc.) the whole operation returned `false` → endpoint responded `500 "Submission failed"` even though the submission was already persisted. After: DB failure still returns `false` (critical path); email failure is logged via `console.error` and the submission succeeds with `200`.
16
+
17
+ ### Notes
18
+
19
+ No SQL migration. No API signature changes. Existing projects that already have a hand-rolled `src/routes/api/forms/[slug]/submit/+server.ts` keep working — scaffold skips existing files unless `--force` is passed.
20
+
21
+ ## 0.15.3 — 2026-04-16
22
+
23
+ Shop: InPost carrier adapter — Geowidget v5 picker + ShipX shipment + webhook auto-status.
24
+
25
+ ### Added
26
+ - `inpostAdapter()` ships in `includio-cms/shop` — wires Geowidget v5 (paczkomat picker on the frontend), ShipX (admin "Utwórz przesyłkę InPost" button creates the shipment, auto-buys the offer, fetches the label PDF) and a webhook receiver (`POST /api/shop/carriers/inpost/webhook?secret=...`) that updates order status + tracking number when ShipX events arrive.
27
+ - `<InpostPicker>` Svelte component (`includio-cms/shop/svelte`) — drop-in widget with `bind:value={carrierRef}`, lazy-loads the geowidget script + CSS, hides itself for `inpost_courier_*` services. Raw API alternative: `GET /api/shop/carriers/inpost`.
28
+ - Per-shipping-method service config — `shop_shipping_methods.carrier_config` jsonb with `serviceType` (`inpost_locker_standard` / `inpost_locker_express` / `inpost_courier_standard` / `inpost_courier_express`) and `defaultSize` (A/B/C → small/medium/large). Admin shipping form ungates the InPost option and shows the new fields when selected.
29
+ - Customer order endpoint exposes `trackingNumber` + `trackingUrl` (resolved via `adapter.trackingUrl()`); `<OrderStatus>` renders a "Śledzenie przesyłki" section automatically. Status emails for `preparing` / `sent` / `done` include a tracking block with a clickable carrier link.
30
+ - Admin order detail page gains a "Przesyłka {carrierType}" panel: Utwórz przesyłkę / Pobierz etykietę PDF / Anuluj przesyłkę. PDF is streamed through an admin-auth proxy at `/api/shop/admin/orders/[id]/label`.
31
+ - Checkout validates `carrierRef` against the carrier (Operating-only paczkomats via InPost Points API, in-memory cached 5 min). Invalid selections reject with HTTP 400 before the order is created.
32
+ - CLI scaffold (`pnpm includio scaffold admin`) emits the new routes: `api/shop/carriers/[id]/+server.ts`, `api/shop/carriers/[id]/webhook/+server.ts`, `api/shop/admin/orders/[id]/label/+server.ts`.
33
+ - CLI scaffolder auto-detects shop usage by scanning `src/lib/cms/cms.config.ts` for an active `shop:` property — projects without the shop module get a clean route tree by default, no flag required. Override with `--shop` / `--no-shop` (or pass `scaffoldAdmin({ shop: false })` programmatically). Combined with the existing config-driven schema generator + admin sidebar guard, shop is now fully opt-in with zero footprint when unused.
34
+
35
+ ### Migration
36
+
37
+ ```sql
38
+ ALTER TABLE shop_shipping_methods ADD COLUMN carrier_config jsonb;
39
+ ALTER TABLE shop_orders ADD COLUMN shipment_id text, ADD COLUMN tracking_number text, ADD COLUMN label_url text, ADD COLUMN shipment_created_at timestamptz;
40
+ ```
41
+
42
+ ### Notes
43
+
44
+ Run the SQL once, then `pnpm db:push` to sync the rest of the schema. InPost requires two tokens: a public Geowidget v5 token (browser-side, from `manager.paczkomaty.pl`) and a private ShipX organization token (server-side). On sandbox, ShipX `POST /shipments/:id/buy` is asynchronous and often stalls in `offer_selected` because test accounts have no wallet to pay the offer — the webhook reconciles whatever ends up. See /docs/shop/inpost for full setup.
45
+
6
46
  ## 0.15.2 — 2026-04-15
7
47
 
8
48
  Shop: cena przechowywana jako numeric(20,6) — eliminacja driftu brutto/netto po reload; toggle netto/brutto per wariant.
package/DOCS.md CHANGED
@@ -1,4 +1,4 @@
1
- # Includio CMS Documentation (v0.15.2)
1
+ # Includio CMS Documentation (v0.15.4)
2
2
 
3
3
  > This file is auto-generated from the docs site. For the latest version, update the package.
4
4
 
@@ -2743,9 +2743,13 @@ Forms can be submitted from your frontend via a public POST endpoint:
2743
2743
 
2744
2744
  ```
2745
2745
  POST /api/forms/{slug}/submit
2746
- Content-Type: multipart/form-data
2746
+ Content-Type: multipart/form-data # or application/json for file-less payloads
2747
2747
  ```
2748
2748
 
2749
+ > **Auto-generated endpoint:** Running `includio scaffold admin` generates `src/routes/api/forms/[slug]/submit/+server.ts` automatically — no manual setup required. To customize (extra middleware, logging, honeypot handling), edit the generated file; it won't be overwritten unless you re-run scaffold with `--force`.
2750
+
2751
+ > **Notification emails are best-effort:** If a form has `notificationEmailAddresses` set and email sending fails (e.g. misconfigured SMTP), the submission is still saved and the endpoint returns `200`. The error is logged to `console.error` — check server logs if you stop receiving notifications.
2752
+
2749
2753
  ### Submitting from Frontend
2750
2754
 
2751
2755
  ```typescript
@@ -4311,6 +4315,7 @@ Headless e-commerce module. Optional — activate by adding `shop: defineShop({.
4311
4315
  - **Cart** — signed cookie, headless SDK (`createShopClient()`), `POST/PATCH/DELETE /api/shop/cart`.
4312
4316
  - **Checkout + orders** — `POST /api/shop/checkout` creates an order with consent validation, stock reservation (30-min TTL), and status emails.
4313
4317
  - **Payment adapters** — `manualAdapter()` (bank transfer / COD) and `payuAdapter()` ship built-in; plug your own by implementing `PaymentAdapter`.
4318
+ - **Carrier adapters** — `inpostAdapter()` ships built-in (Geowidget v5 picker + ShipX shipments + webhook). Plug other carriers by implementing `CarrierAdapter`.
4314
4319
  - **Webhook infrastructure** — `createPaymentWebhookHandler()` dispatches provider callbacks, verifies signatures, is idempotent.
4315
4320
  - **Secure order view** — per-order `accessToken` + cookie fallback + `GET /api/shop/orders/[number]` token-gated API.
4316
4321
  - **Shipping ↔ payment compatibility** — restrict payment methods per shipping (e.g. no COD for paczkomat).
@@ -4344,7 +4349,18 @@ export const cmsConfig = defineCMS({
4344
4349
 
4345
4350
  ## Routes
4346
4351
 
4347
- Scaffold provisions these endpoints in your app:
4352
+ Shop is **opt-in**: projects without `shop: defineShop(...)` in their CMS config don't get shop tables in the DB schema (generator gates on `config.shop`), the admin sidebar hides the Shop section, and shop API handlers always return 404.
4353
+
4354
+ The CLI scaffolder **auto-detects** shop usage by scanning your `src/lib/cms/cms.config.ts` for `shop: …`. Override with `--shop` or `--no-shop` if needed:
4355
+
4356
+ ```sh
4357
+ pnpm includio scaffold admin # auto: detects shop in cms config
4358
+ pnpm includio scaffold admin --shop # force include shop routes
4359
+ pnpm includio scaffold admin --no-shop # force exclude shop routes
4360
+ pnpm includio scaffold admin --cms-config path/to/config.ts # custom location
4361
+ ```
4362
+
4363
+ Scaffold provisions these endpoints (when shop is enabled):
4348
4364
 
4349
4365
  | Path | Handler | Purpose |
4350
4366
  |------|---------|---------|
@@ -4355,11 +4371,15 @@ Scaffold provisions these endpoints in your app:
4355
4371
  | `POST /api/shop/orders/[number]/refresh-payment` | `createRefreshPaymentHandler()` | Pull status from provider |
4356
4372
  | `POST /api/shop/orders/[number]/retry-payment` | `createRetryPaymentHandler()` | New payment attempt |
4357
4373
  | `POST /api/shop/webhooks/[provider]` | `createPaymentWebhookHandler()` | Provider callbacks |
4374
+ | `GET /api/shop/carriers/[id]` | `createCarrierConfigHandler()` | Public carrier widget descriptor |
4375
+ | `POST /api/shop/carriers/[id]/webhook` | `createCarrierWebhookHandler()` | Carrier event receiver |
4376
+ | `GET /api/shop/admin/orders/[id]/label` | `createShipmentLabelHandler()` | Admin shipping-label PDF proxy |
4358
4377
 
4359
4378
  ## See also
4360
4379
 
4361
4380
  - [Order view](/docs/shop/order-view) — generic `<OrderStatus>` component + headless helper
4362
4381
  - [Retry payment](/docs/shop/retry-payment) — lifecycle + endpoint
4382
+ - [InPost carrier](/docs/shop/inpost) — Geowidget v5 + ShipX shipment + webhook
4363
4383
 
4364
4384
 
4365
4385
  ---
@@ -4527,6 +4547,125 @@ if (url) window.location.href = url;
4527
4547
  If the customer is completely stuck, the admin can manually move the order to `paid` / `cancelled` from the orders detail page — same as before, status-change emails trigger automatically.
4528
4548
 
4529
4549
 
4550
+ ---
4551
+
4552
+ # InPost carrier
4553
+
4554
+ `inpostAdapter()` ships in 0.15.3. Wires three things together:
4555
+
4556
+ 1. **Geowidget v5** — customer picks a paczkomat on the checkout page (`<InpostPicker>` Svelte component or raw config endpoint).
4557
+ 2. **ShipX shipment** — admin clicks "Utwórz przesyłkę InPost" on the order detail page → backend POSTs the shipment to ShipX, auto-confirms the offer, stores the carrier-side shipment id + tracking number.
4558
+ 3. **Webhook** — ShipX events (`confirmed`/`taken_by_courier`/`delivered`) update the order status and tracking number automatically.
4559
+
4560
+ > InPost requires two separate tokens: a **public Geowidget v5 token** (delivered to the browser) and a **private ShipX organization token** (server-side only). Both come from `manager.paczkomaty.pl` (production) or `sandbox-manager.paczkomaty.pl` (sandbox).
4561
+
4562
+ ## Config
4563
+
4564
+ ```ts
4565
+ import { defineShop, inpostAdapter } from 'includio-cms/shop';
4566
+
4567
+ defineShop({
4568
+ // …currency, payment, etc.
4569
+ carriers: [
4570
+ inpostAdapter({
4571
+ geowidgetToken: process.env.INPOST_GEOWIDGET_TOKEN!,
4572
+ shipxToken: process.env.INPOST_SHIPX_TOKEN!,
4573
+ organizationId: process.env.INPOST_ORG_ID!,
4574
+ environment: 'production', // or 'sandbox'
4575
+ webhookSecret: process.env.INPOST_WEBHOOK_SECRET!,
4576
+ senderAddress: {
4577
+ name: 'Twoja firma',
4578
+ company: 'AriaCMS',
4579
+ street: 'Mokotowska',
4580
+ buildingNumber: '1',
4581
+ city: 'Warszawa',
4582
+ postCode: '00-001',
4583
+ email: 'shop@example.com',
4584
+ phone: '+48500600700'
4585
+ }
4586
+ })
4587
+ ]
4588
+ });
4589
+ ```
4590
+
4591
+ **Optional opts:**
4592
+
4593
+ - `additionalServices: ['email', 'sms']` — InPost SMS/email notifications. Requires the service to be enabled on your account; sandbox usually doesn't have it.
4594
+ - `autoConfirm: false` — disable the post-create `POST /shipments/:id/buy` step. Default `true`.
4595
+ - `autoConfirmDelayMs` (default `1500`), `autoConfirmPollTimeoutMs` (default `8000`) — tune the offer-prep wait and post-buy polling.
4596
+ - `trackingUrlTemplate` — override `https://inpost.pl/sledzenie-przesylek?number={trackingNumber}`.
4597
+ - `labelFormat: 'Pdf' | 'ZebraLP'`, `labelSize: 'A4' | 'A6'` — default label format.
4598
+ - `debug: true` — verbose logging of the create→buy→poll flow (payload dump, every poll tick). Off by default; warnings + errors always log.
4599
+
4600
+ ## Routes scaffolded
4601
+
4602
+ `pnpm includio scaffold admin` provisions:
4603
+
4604
+ | Path | Handler | Purpose |
4605
+ |------|---------|---------|
4606
+ | `GET /api/shop/carriers/[id]` | `createCarrierConfigHandler()` | Public widget descriptor (script URL + token + preset) |
4607
+ | `POST /api/shop/carriers/[id]/webhook` | `createCarrierWebhookHandler()` | ShipX event receiver — secret-gated via `?secret=…` |
4608
+ | `GET /api/shop/admin/orders/[id]/label` | `createShipmentLabelHandler()` | Admin-only PDF proxy for the shipment label |
4609
+
4610
+ In `manager.paczkomaty.pl` configure the webhook URL as
4611
+ `https://your-domain.pl/api/shop/carriers/inpost/webhook?secret=<INPOST_WEBHOOK_SECRET>`.
4612
+
4613
+ ## Per-shipping-method service
4614
+
4615
+ Each shipping method with `carrierType=inpost` carries a `carrierConfig`:
4616
+
4617
+ | Field | Values |
4618
+ |------|--------|
4619
+ | `serviceType` | `inpost_locker_standard` (paczkomat), `inpost_locker_express`, `inpost_courier_standard`, `inpost_courier_express` |
4620
+ | `defaultSize` | `A` (small, 8×38×64 cm) / `B` (medium, 19×38×64 cm) / `C` (large, 41×38×64 cm) |
4621
+
4622
+ Set both in admin → Shop → Shipping methods. For `inpost_courier_*` services the picker hides itself and the receiver address from the order is used instead.
4623
+
4624
+ ## Frontend picker
4625
+
4626
+ ```svelte
4627
+ <InpostPicker
4628
+ bind:value={carrierRef}
4629
+ serviceType="inpost_locker_standard"
4630
+ onSelect={(point) => console.log('picked', point.name, point.address)}
4631
+ />
4632
+ ```
4633
+
4634
+ The component lazy-loads the Geowidget v5 script + CSS, mounts the `<inpost-geowidget>` custom element, and writes the chosen paczkomat code into `value`. For `inpost_courier_*` services it renders a short notice instead.
4635
+
4636
+ **Custom UI** — skip the component and call `GET /api/shop/carriers/inpost` yourself; the response is `{ id, label, widget: { scriptUrl, stylesheetUrl, config: { token, language, config } } }`.
4637
+
4638
+ ## Admin actions
4639
+
4640
+ On any order with an InPost shipping method, the order detail page shows:
4641
+
4642
+ - **"Utwórz przesyłkę InPost"** when status is `paid`/`preparing` and no shipment exists yet — POSTs to ShipX, auto-buys the offer, polls until status leaves `offer_selected`, writes `shipmentId` + `trackingNumber` on the order, bumps status to `preparing`.
4643
+ - **"Pobierz etykietę PDF"** — opens the admin label proxy in a new tab.
4644
+ - **"Anuluj przesyłkę"** — `DELETE /v1/shipments/:id` on ShipX, clears local data. Only works after the shipment reaches `confirmed`.
4645
+
4646
+ ## Webhook → status mapping
4647
+
4648
+ | ShipX event status | Order status |
4649
+ |---|---|
4650
+ | `created`, `offers_prepared`, `offer_selected`, `confirmed`, `dispatched_by_sender` | `preparing` |
4651
+ | `taken_by_courier`, `adopted_at_*`, `out_for_delivery`, `ready_to_pickup` | `sent` |
4652
+ | `delivered` | `done` |
4653
+ | `canceled`, `returned_to_sender`, `rejected` | `cancelled` |
4654
+ | anything else | (ignored) |
4655
+
4656
+ Tracking number is updated whenever the webhook payload contains a non-null `tracking_number`. Status emails (built-in PL/EN templates) include a tracking block on `preparing`/`sent`/`done`.
4657
+
4658
+ ## Sandbox quirks
4659
+
4660
+ - ShipX `POST /shipments/:id/buy` is **asynchronous**. Production confirms in ~1–3 s; sandbox often **stalls in `offer_selected`** because test accounts have no wallet to pay the offer. The adapter polls for ~8 s then gives up and lets the webhook reconcile.
4661
+ - Sandbox geowidget paczkomaty are **separate** from production codes. Use `KRA012` (Kraków, Os. Kombatantów 20) or any code from `GET https://sandbox-api-shipx-pl.easypack24.net/v1/points?per_page=10`.
4662
+ - Sandbox `additional_services: ['email', 'sms']` returns `unavailable` — drop the option for sandbox testing.
4663
+
4664
+ ## Customer tracking display
4665
+
4666
+ `OrderStatus.svelte` (the built-in component) renders a "Śledzenie przesyłki" section automatically when the order has a `trackingNumber`. If you wrote a custom order view, read `order.trackingNumber` and `order.trackingUrl` from the order endpoint response and render them yourself.
4667
+
4668
+
4530
4669
  ---
4531
4670
 
4532
4671
  # Migration Guide
package/ROADMAP.md CHANGED
@@ -304,8 +304,8 @@
304
304
  - [x] `[feature]` `[P0]` Random Crockford base32 order numbers (`XXXXX-XXXXX`), unguessable
305
305
  - [x] `[feature]` `[P0]` CLI scaffold provisions all shop routes (admin + public API) in consumer app
306
306
  - [x] `[feature]` `[P1]` PayU payment adapter + webhook — shipped in 0.15.1 (access-token-gated order view, idempotent webhooks, MD5 signature, shipping↔payment compat, poll fallback)
307
- - [ ] `[feature]` `[P1]` Stripe payment adapter + webhook — deferred to 0.15.3
308
- - [ ] `[feature]` `[P2]` InPost carrier adapter (headless geowidget config) deferred to 0.15.3
307
+ - [ ] `[feature]` `[P1]` Stripe payment adapter + webhook — deferred to 0.15.4
308
+ - [x] `[feature]` `[P2]` InPost carrier adapter — shipped in 0.15.3 (Geowidget v5 picker + ShipX shipments + auto-confirm + webhook status updates)
309
309
 
310
310
  ## 0.15.2 — Price precision fix
311
311
 
@@ -313,6 +313,17 @@
313
313
  - [x] `[feature]` `[P2]` Variant price toggle netto/brutto (spójne z toggle\'em ceny bazowej)
314
314
  - [x] `[feature]` `[P2]` `nodemailerAdapter` — typowanie `transportOptions` jako `SMTPTransport.Options` (OAuth2, pool, itd.)
315
315
 
316
+ ## 0.15.3 — InPost carrier
317
+
318
+ - [x] `[feature]` `[P2]` `inpostAdapter()` — Geowidget v5 (`<InpostPicker>` Svelte + raw config endpoint), ShipX shipment create + auto-buy + label PDF + cancel, webhook → status + tracking, per-shipping-method service config, customer tracking display in `<OrderStatus>` + email templates
319
+ - [x] `[chore]` `[P2]` Verbose logging on adapter auto-confirm flow (offer prep wait, buy POST, polling) for sandbox debugging
320
+
321
+ ## 0.15.4 — Forms submission scaffold + best-effort notification emails
322
+
323
+ - [x] `[feature]` `[P1]` CLI scaffold emits `src/routes/api/forms/[slug]/submit/+server.ts` — public form submit endpoint auto-generated, no manual setup per project <!-- files: src/lib/cli/scaffold/admin.ts, src/lib/cli/scaffold/admin.spec.ts -->
324
+ - [x] `[fix]` `[P0]` `createFormSubmission` — split try/catch so SMTP failure no longer returns `false` (endpoint responded 500 even though submission was persisted); notification email is best-effort, logged via `console.error` <!-- files: src/lib/core/server/forms/submissions/operations/create.ts -->
325
+ - [ ] `[feature]` `[P1]` Built-in `/api/health` + `/api/health/ready` with per-adapter checks (db/files/email/ai) + ręczny SMTP diagnostics panel in maintenance page <!-- files: ideas/health-check-module.md -->
326
+
316
327
  ## 0.16.0 — SEO module
317
328
 
318
329
  - [ ] `[feature]` `[P1]` SERP preview + character limits for title/description <!-- files: src/lib/admin/components/fields/seo-field.svelte -->
@@ -3,6 +3,16 @@
3
3
  import { Switch } from '../../../components/ui/switch/index.js';
4
4
 
5
5
  type CarrierType = 'none' | 'inpost';
6
+ type InpostServiceType =
7
+ | 'inpost_locker_standard'
8
+ | 'inpost_locker_express'
9
+ | 'inpost_courier_standard'
10
+ | 'inpost_courier_express';
11
+ type InpostParcelSize = 'A' | 'B' | 'C';
12
+ interface CarrierConfig {
13
+ serviceType?: InpostServiceType | string;
14
+ defaultSize?: InpostParcelSize | string;
15
+ }
6
16
  export interface PaymentMethodOption {
7
17
  id: string;
8
18
  label: Record<string, string>;
@@ -13,6 +23,7 @@
13
23
  price: number; // PLN (number, netto)
14
24
  vatRate: number;
15
25
  carrierType: CarrierType;
26
+ carrierConfig: CarrierConfig | null;
16
27
  conditions: { freeAbove?: number } | null; // freeAbove: grosze
17
28
  allowedPaymentMethods: string[] | null;
18
29
  isActive: boolean;
@@ -24,6 +35,7 @@
24
35
  price?: number | string;
25
36
  vatRate?: number;
26
37
  carrierType?: string;
38
+ carrierConfig?: CarrierConfig | null;
27
39
  conditions?: { freeAbove?: number } | null;
28
40
  allowedPaymentMethods?: string[] | null;
29
41
  isActive?: boolean;
@@ -97,6 +109,12 @@
97
109
  inputPrice = formatPln(preserved);
98
110
  }
99
111
  let carrierType = $state<CarrierType>((initial?.carrierType as CarrierType) ?? 'none');
112
+ let inpostServiceType = $state<InpostServiceType>(
113
+ (initial?.carrierConfig?.serviceType as InpostServiceType) ?? 'inpost_locker_standard'
114
+ );
115
+ let inpostDefaultSize = $state<InpostParcelSize>(
116
+ (initial?.carrierConfig?.defaultSize as InpostParcelSize) ?? 'A'
117
+ );
100
118
  let isActive = $state(initial?.isActive ?? true);
101
119
  // null/undefined = no restriction (all allowed); otherwise a whitelist.
102
120
  let restrictPayments = $state(
@@ -132,6 +150,10 @@
132
150
  price: netPln,
133
151
  vatRate: Number(vatRate),
134
152
  carrierType,
153
+ carrierConfig:
154
+ carrierType === 'inpost'
155
+ ? { serviceType: inpostServiceType, defaultSize: inpostDefaultSize }
156
+ : null,
135
157
  conditions: freeAboveEnabled
136
158
  ? { freeAbove: Math.round(parseFloat(freeAbove || '0') * 100) }
137
159
  : null,
@@ -300,9 +322,52 @@
300
322
  <span class="mb-1 block text-sm font-semibold">Typ</span>
301
323
  <select bind:value={carrierType} class="border-border w-full rounded-lg border px-3 py-2">
302
324
  <option value="none">Bez integracji (adres)</option>
303
- <option value="inpost" disabled>InPost paczkomat (wkrótce)</option>
325
+ <option value="inpost">InPost</option>
304
326
  </select>
305
327
  </label>
328
+
329
+ {#if carrierType === 'inpost'}
330
+ <div class="space-y-4 border-l-2 border-primary/30 pl-4">
331
+ <p class="text-muted-foreground text-sm">
332
+ Wymaga skonfigurowanego adaptera <code>inpostAdapter</code> w <code>cms.config.ts</code>.
333
+ </p>
334
+
335
+ <label class="block">
336
+ <span class="mb-1 block text-sm font-semibold">Usługa ShipX</span>
337
+ <select
338
+ bind:value={inpostServiceType}
339
+ class="border-border w-full rounded-lg border px-3 py-2"
340
+ >
341
+ <option value="inpost_locker_standard">Paczkomat — standard</option>
342
+ <option value="inpost_locker_express">Paczkomat — Express</option>
343
+ <option value="inpost_courier_standard">Kurier — standard</option>
344
+ <option value="inpost_courier_express">Kurier — Express</option>
345
+ </select>
346
+ </label>
347
+
348
+ <label class="block">
349
+ <span class="mb-1 block text-sm font-semibold">Domyślny rozmiar paczki</span>
350
+ <select
351
+ bind:value={inpostDefaultSize}
352
+ class="border-border w-full rounded-lg border px-3 py-2"
353
+ >
354
+ <option value="A">A — mała (8 × 38 × 64 cm)</option>
355
+ <option value="B">B — średnia (19 × 38 × 64 cm)</option>
356
+ <option value="C">C — duża (41 × 38 × 64 cm)</option>
357
+ </select>
358
+ </label>
359
+
360
+ {#if inpostServiceType.startsWith('inpost_courier_')}
361
+ <p class="text-muted-foreground text-xs">
362
+ Kurier dostarcza pod adres — klient nie wybiera paczkomatu.
363
+ </p>
364
+ {:else}
365
+ <p class="text-muted-foreground text-xs">
366
+ Klient wybiera paczkomat odbiorczy w geowidgetcie InPost.
367
+ </p>
368
+ {/if}
369
+ </div>
370
+ {/if}
306
371
  </section>
307
372
 
308
373
  <div class="flex gap-3">
@@ -1,4 +1,10 @@
1
1
  type CarrierType = 'none' | 'inpost';
2
+ type InpostServiceType = 'inpost_locker_standard' | 'inpost_locker_express' | 'inpost_courier_standard' | 'inpost_courier_express';
3
+ type InpostParcelSize = 'A' | 'B' | 'C';
4
+ interface CarrierConfig {
5
+ serviceType?: InpostServiceType | string;
6
+ defaultSize?: InpostParcelSize | string;
7
+ }
2
8
  export interface PaymentMethodOption {
3
9
  id: string;
4
10
  label: Record<string, string>;
@@ -9,6 +15,7 @@ export interface ShippingFormPayload {
9
15
  price: number;
10
16
  vatRate: number;
11
17
  carrierType: CarrierType;
18
+ carrierConfig: CarrierConfig | null;
12
19
  conditions: {
13
20
  freeAbove?: number;
14
21
  } | null;
@@ -22,6 +29,7 @@ export interface ShippingFormInitial {
22
29
  price?: number | string;
23
30
  vatRate?: number;
24
31
  carrierType?: string;
32
+ carrierConfig?: CarrierConfig | null;
25
33
  conditions?: {
26
34
  freeAbove?: number;
27
35
  } | null;
@@ -45,6 +45,7 @@
45
45
  let note = $state('');
46
46
  let saving = $state(false);
47
47
  let resending = $state(false);
48
+ let shipping = $state(false);
48
49
  let errorMessage = $state<string | null>(null);
49
50
  let successMessage = $state<string | null>(null);
50
51
 
@@ -102,6 +103,49 @@
102
103
  resending = false;
103
104
  }
104
105
  }
106
+
107
+ async function handleCreateShipment() {
108
+ shipping = true;
109
+ errorMessage = null;
110
+ successMessage = null;
111
+ try {
112
+ const result = await remotes.createShipmentForOrderCmd({ orderId });
113
+ if (!result.success) {
114
+ errorMessage = result.error;
115
+ } else {
116
+ successMessage = result.trackingNumber
117
+ ? `Przesyłka utworzona — ${result.trackingNumber}`
118
+ : 'Przesyłka utworzona. Numer śledzenia pojawi się po potwierdzeniu w ShipX.';
119
+ await query.refresh();
120
+ }
121
+ } catch (err) {
122
+ errorMessage =
123
+ err instanceof Error ? err.message : 'Błąd przy tworzeniu przesyłki';
124
+ } finally {
125
+ shipping = false;
126
+ }
127
+ }
128
+
129
+ async function handleCancelShipment() {
130
+ if (!confirm('Anulować przesyłkę? Etykieta zostanie unieważniona w ShipX.')) return;
131
+ shipping = true;
132
+ errorMessage = null;
133
+ successMessage = null;
134
+ try {
135
+ const result = await remotes.cancelShipmentForOrderCmd({ orderId });
136
+ if (!result.success) {
137
+ errorMessage = result.error;
138
+ } else {
139
+ successMessage = 'Przesyłka anulowana.';
140
+ await query.refresh();
141
+ }
142
+ } catch (err) {
143
+ errorMessage =
144
+ err instanceof Error ? err.message : 'Błąd przy anulowaniu przesyłki';
145
+ } finally {
146
+ shipping = false;
147
+ }
148
+ }
105
149
  </script>
106
150
 
107
151
  {#if !query.ready}
@@ -268,6 +312,63 @@
268
312
  </Button>
269
313
  </section>
270
314
 
315
+ {#if order.carrierType && order.carrierType !== 'none'}
316
+ <section class="border-border bg-card space-y-3 rounded-xl border p-5 text-sm">
317
+ <h2 class="text-base font-bold">
318
+ Przesyłka <span class="text-muted-foreground font-mono text-xs">{order.carrierType}</span>
319
+ </h2>
320
+ {#if order.shipmentId}
321
+ <div class="space-y-1">
322
+ <div class="text-muted-foreground text-xs font-semibold">Shipment ID</div>
323
+ <div class="font-mono text-xs break-all">{order.shipmentId}</div>
324
+ </div>
325
+ {#if order.trackingNumber}
326
+ <div class="space-y-1">
327
+ <div class="text-muted-foreground text-xs font-semibold">Numer śledzenia</div>
328
+ <div class="font-mono text-xs break-all">{order.trackingNumber}</div>
329
+ </div>
330
+ {:else}
331
+ <p class="text-xs text-amber-700">
332
+ Numer śledzenia pojawi się gdy ShipX potwierdzi przesyłkę (zwykle w kilka minut).
333
+ </p>
334
+ {/if}
335
+ <div class="flex flex-wrap gap-2 pt-2">
336
+ <Button
337
+ href="/api/shop/admin/orders/{orderId}/label"
338
+ variant="outline"
339
+ size="sm"
340
+ target="_blank"
341
+ >
342
+ Pobierz etykietę PDF
343
+ </Button>
344
+ <Button
345
+ onclick={handleCancelShipment}
346
+ disabled={shipping}
347
+ variant="outline"
348
+ size="sm"
349
+ >
350
+ Anuluj przesyłkę
351
+ </Button>
352
+ </div>
353
+ {:else if order.status === 'paid' || order.status === 'preparing'}
354
+ <p class="text-muted-foreground text-xs">
355
+ Wygeneruj etykietę i nadaj paczkę przez ShipX.
356
+ </p>
357
+ <Button
358
+ onclick={handleCreateShipment}
359
+ disabled={shipping}
360
+ class="w-full"
361
+ >
362
+ {shipping ? 'Tworzenie…' : 'Utwórz przesyłkę InPost'}
363
+ </Button>
364
+ {:else}
365
+ <p class="text-muted-foreground text-xs">
366
+ Przesyłkę można utworzyć po opłaceniu zamówienia.
367
+ </p>
368
+ {/if}
369
+ </section>
370
+ {/if}
371
+
271
372
  <section class="border-border bg-card space-y-2 rounded-xl border p-5 text-sm">
272
373
  <h2 class="text-base font-bold">Klient</h2>
273
374
  <div>
@@ -39,6 +39,10 @@ export declare const createShippingMethodCmd: import("@sveltejs/kit").RemoteComm
39
39
  vatRate: number;
40
40
  description?: Record<string, string> | null | undefined;
41
41
  carrierType?: string | undefined;
42
+ carrierConfig?: {
43
+ serviceType?: string | undefined;
44
+ defaultSize?: string | undefined;
45
+ } | null | undefined;
42
46
  conditions?: {
43
47
  freeAbove?: number | undefined;
44
48
  } | null | undefined;
@@ -54,6 +58,10 @@ export declare const updateShippingMethodCmd: import("@sveltejs/kit").RemoteComm
54
58
  price?: number | undefined;
55
59
  vatRate?: number | undefined;
56
60
  carrierType?: string | undefined;
61
+ carrierConfig?: {
62
+ serviceType?: string | undefined;
63
+ defaultSize?: string | undefined;
64
+ } | null | undefined;
57
65
  conditions?: {
58
66
  freeAbove?: number | undefined;
59
67
  } | null | undefined;
@@ -99,6 +107,10 @@ export declare const listOrdersAdmin: import("@sveltejs/kit").RemoteQueryFunctio
99
107
  shippingGross: number;
100
108
  shippingMethodId: string | null;
101
109
  carrierRef: string | null;
110
+ shipmentId: string | null;
111
+ trackingNumber: string | null;
112
+ labelUrl: string | null;
113
+ shipmentCreatedAt: Date | null;
102
114
  paymentMethod: string | null;
103
115
  paymentProviderRef: string | null;
104
116
  notes: string | null;
@@ -130,6 +142,10 @@ export declare const getOrderForAdmin: import("@sveltejs/kit").RemoteQueryFuncti
130
142
  shippingGross: number;
131
143
  shippingMethodId: string | null;
132
144
  carrierRef: string | null;
145
+ shipmentId: string | null;
146
+ trackingNumber: string | null;
147
+ labelUrl: string | null;
148
+ shipmentCreatedAt: Date | null;
133
149
  paymentMethod: string | null;
134
150
  paymentProviderRef: string | null;
135
151
  notes: string | null;
@@ -185,6 +201,10 @@ export declare const updateOrderStatusCmd: import("@sveltejs/kit").RemoteCommand
185
201
  shippingGross: number;
186
202
  shippingMethodId: string | null;
187
203
  carrierRef: string | null;
204
+ shipmentId: string | null;
205
+ trackingNumber: string | null;
206
+ labelUrl: string | null;
207
+ shipmentCreatedAt: Date | null;
188
208
  paymentMethod: string | null;
189
209
  paymentProviderRef: string | null;
190
210
  notes: string | null;
@@ -195,6 +215,30 @@ export declare const resendOrderEmailCmd: import("@sveltejs/kit").RemoteCommand<
195
215
  }, Promise<{
196
216
  success: boolean;
197
217
  }>>;
218
+ export declare const createShipmentForOrderCmd: import("@sveltejs/kit").RemoteCommand<{
219
+ orderId: string;
220
+ }, Promise<{
221
+ success: true;
222
+ shipmentId: string | null;
223
+ trackingNumber: string | null;
224
+ status: import("../../shop/types.js").OrderStatus;
225
+ error?: undefined;
226
+ } | {
227
+ success: false;
228
+ error: string;
229
+ shipmentId?: undefined;
230
+ trackingNumber?: undefined;
231
+ status?: undefined;
232
+ }>>;
233
+ export declare const cancelShipmentForOrderCmd: import("@sveltejs/kit").RemoteCommand<{
234
+ orderId: string;
235
+ }, Promise<{
236
+ success: true;
237
+ error?: undefined;
238
+ } | {
239
+ success: false;
240
+ error: string;
241
+ }>>;
198
242
  export declare const listShopableCollections: import("@sveltejs/kit").RemoteQueryFunction<void, {
199
243
  slug: string;
200
244
  labels: {
@@ -4,6 +4,7 @@ import { getCMS } from '../../core/cms.js';
4
4
  import { deleteShopData, getShopDataByEntry, listShopEntries, upsertShopData } from '../../shop/server/shop-data.js';
5
5
  import { createShippingMethod, deleteShippingMethod, getShippingMethod, listShippingMethods, reorderShippingMethods, updateShippingMethod } from '../../shop/server/shipping.js';
6
6
  import { getOrderById, getOrderItems, getOrderStatusHistory, listOrders, updateOrderStatus } from '../../shop/server/orders.js';
7
+ import { cancelShipmentForOrder, createShipmentForOrder } from '../../shop/server/shipments.js';
7
8
  import { sendOrderStatusEmail } from '../../shop/server/email.js';
8
9
  import { requireAuth } from './middleware/auth.js';
9
10
  export const getShopEnabled = query(async () => {
@@ -63,6 +64,13 @@ const shippingMethodInputSchema = z.object({
63
64
  price: z.number().nonnegative().max(1e9), // PLN netto (≤6dp)
64
65
  vatRate: z.number().int().min(0).max(100),
65
66
  carrierType: z.string().optional(),
67
+ carrierConfig: z
68
+ .object({
69
+ serviceType: z.string().optional(),
70
+ defaultSize: z.string().optional()
71
+ })
72
+ .nullable()
73
+ .optional(),
66
74
  conditions: z
67
75
  .object({ freeAbove: z.number().int().nonnegative().optional() })
68
76
  .nullable()
@@ -143,6 +151,33 @@ export const resendOrderEmailCmd = command(z.object({ orderId: z.string(), statu
143
151
  await sendOrderStatusEmail(orderId, status);
144
152
  return { success: true };
145
153
  });
154
+ export const createShipmentForOrderCmd = command(z.object({ orderId: z.string() }), async ({ orderId }) => {
155
+ requireAuth();
156
+ try {
157
+ const order = await createShipmentForOrder(orderId);
158
+ return {
159
+ success: true,
160
+ shipmentId: order.shipmentId,
161
+ trackingNumber: order.trackingNumber,
162
+ status: order.status
163
+ };
164
+ }
165
+ catch (err) {
166
+ const message = err instanceof Error ? err.message : 'Shipment create failed';
167
+ return { success: false, error: message };
168
+ }
169
+ });
170
+ export const cancelShipmentForOrderCmd = command(z.object({ orderId: z.string() }), async ({ orderId }) => {
171
+ requireAuth();
172
+ try {
173
+ await cancelShipmentForOrder(orderId);
174
+ return { success: true };
175
+ }
176
+ catch (err) {
177
+ const message = err instanceof Error ? err.message : 'Shipment cancel failed';
178
+ return { success: false, error: message };
179
+ }
180
+ });
146
181
  export const listShopableCollections = query(async () => {
147
182
  requireAuth();
148
183
  const cms = getCMS();