includio-cms 0.15.1 → 0.15.3

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 (89) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/DOCS.md +137 -2
  3. package/ROADMAP.md +12 -2
  4. package/dist/admin/client/shop/shipping-method-form.svelte +90 -22
  5. package/dist/admin/client/shop/shipping-method-form.svelte.d.ts +9 -1
  6. package/dist/admin/client/shop/shipping-methods-list-page.svelte +7 -4
  7. package/dist/admin/client/shop/shop-order-detail-page.svelte +101 -0
  8. package/dist/admin/client/shop/shop-products-list-page.svelte +2 -2
  9. package/dist/admin/components/fields/shop-field.svelte +63 -22
  10. package/dist/admin/remote/shop.remote.d.ts +48 -60
  11. package/dist/admin/remote/shop.remote.js +38 -3
  12. package/dist/cli/index.js +49 -4
  13. package/dist/cli/scaffold/admin.d.ts +9 -2
  14. package/dist/cli/scaffold/admin.js +32 -3
  15. package/dist/db-postgres/schema/shop/order.d.ts +68 -0
  16. package/dist/db-postgres/schema/shop/order.js +4 -0
  17. package/dist/db-postgres/schema/shop/product.d.ts +4 -4
  18. package/dist/db-postgres/schema/shop/product.js +3 -2
  19. package/dist/db-postgres/schema/shop/productVariant.d.ts +4 -4
  20. package/dist/db-postgres/schema/shop/productVariant.js +3 -2
  21. package/dist/db-postgres/schema/shop/shippingMethod.d.ts +29 -4
  22. package/dist/db-postgres/schema/shop/shippingMethod.js +4 -2
  23. package/dist/paraglide/messages/_index.d.ts +36 -3
  24. package/dist/paraglide/messages/_index.js +71 -3
  25. package/dist/paraglide/messages/en.d.ts +5 -0
  26. package/dist/paraglide/messages/en.js +14 -0
  27. package/dist/paraglide/messages/pl.d.ts +5 -0
  28. package/dist/paraglide/messages/pl.js +14 -0
  29. package/dist/shop/adapters/inpost/geowidget.d.ts +27 -0
  30. package/dist/shop/adapters/inpost/geowidget.js +31 -0
  31. package/dist/shop/adapters/inpost/index.d.ts +89 -0
  32. package/dist/shop/adapters/inpost/index.js +156 -0
  33. package/dist/shop/adapters/inpost/payload.d.ts +18 -0
  34. package/dist/shop/adapters/inpost/payload.js +85 -0
  35. package/dist/shop/adapters/inpost/points-api.d.ts +17 -0
  36. package/dist/shop/adapters/inpost/points-api.js +55 -0
  37. package/dist/shop/adapters/inpost/shipx-client.d.ts +56 -0
  38. package/dist/shop/adapters/inpost/shipx-client.js +95 -0
  39. package/dist/shop/adapters/inpost/status-map.d.ts +9 -0
  40. package/dist/shop/adapters/inpost/status-map.js +46 -0
  41. package/dist/shop/adapters/inpost/webhook.d.ts +16 -0
  42. package/dist/shop/adapters/inpost/webhook.js +55 -0
  43. package/dist/shop/client/index.d.ts +6 -0
  44. package/dist/shop/http/carrier-handler.d.ts +12 -0
  45. package/dist/shop/http/carrier-handler.js +45 -0
  46. package/dist/shop/http/carrier-webhook-handler.d.ts +13 -0
  47. package/dist/shop/http/carrier-webhook-handler.js +66 -0
  48. package/dist/shop/http/checkout-handler.js +23 -1
  49. package/dist/shop/http/index.d.ts +3 -0
  50. package/dist/shop/http/index.js +3 -0
  51. package/dist/shop/http/order-handler.js +14 -0
  52. package/dist/shop/http/shipment-label-handler.d.ts +10 -0
  53. package/dist/shop/http/shipment-label-handler.js +53 -0
  54. package/dist/shop/http/shipping-handler.js +3 -0
  55. package/dist/shop/index.d.ts +3 -1
  56. package/dist/shop/index.js +1 -0
  57. package/dist/shop/pricing.d.ts +4 -0
  58. package/dist/shop/pricing.js +18 -0
  59. package/dist/shop/server/cart-hydrate.js +6 -3
  60. package/dist/shop/server/email.js +37 -0
  61. package/dist/shop/server/orders.d.ts +9 -0
  62. package/dist/shop/server/orders.js +48 -0
  63. package/dist/shop/server/populate.d.ts +2 -0
  64. package/dist/shop/server/shipments.d.ts +33 -0
  65. package/dist/shop/server/shipments.js +145 -0
  66. package/dist/shop/server/shipping.d.ts +13 -5
  67. package/dist/shop/server/shipping.js +30 -14
  68. package/dist/shop/server/shop-data.d.ts +8 -2
  69. package/dist/shop/server/shop-data.js +18 -10
  70. package/dist/shop/svelte/InpostPicker.svelte +270 -0
  71. package/dist/shop/svelte/InpostPicker.svelte.d.ts +51 -0
  72. package/dist/shop/svelte/OrderStatus.svelte +53 -1
  73. package/dist/shop/svelte/index.d.ts +1 -0
  74. package/dist/shop/svelte/index.js +1 -0
  75. package/dist/shop/svelte/labels.d.ts +5 -0
  76. package/dist/shop/svelte/labels.js +6 -1
  77. package/dist/shop/types.d.ts +49 -1
  78. package/dist/updates/0.15.2/index.d.ts +2 -0
  79. package/dist/updates/0.15.2/index.js +18 -0
  80. package/dist/updates/0.15.3/index.d.ts +2 -0
  81. package/dist/updates/0.15.3/index.js +19 -0
  82. package/dist/updates/index.js +3 -1
  83. package/package.json +1 -1
  84. package/dist/paraglide/messages/hello_world.d.ts +0 -5
  85. package/dist/paraglide/messages/hello_world.js +0 -33
  86. package/dist/paraglide/messages/login_hello.d.ts +0 -16
  87. package/dist/paraglide/messages/login_hello.js +0 -34
  88. package/dist/paraglide/messages/login_please_login.d.ts +0 -16
  89. package/dist/paraglide/messages/login_please_login.js +0 -34
package/CHANGELOG.md CHANGED
@@ -3,6 +3,58 @@
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.3 — 2026-04-16
7
+
8
+ Shop: InPost carrier adapter — Geowidget v5 picker + ShipX shipment + webhook auto-status.
9
+
10
+ ### Added
11
+ - `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.
12
+ - `<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`.
13
+ - 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.
14
+ - 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.
15
+ - 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`.
16
+ - 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.
17
+ - 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`.
18
+ - 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.
19
+
20
+ ### Migration
21
+
22
+ ```sql
23
+ ALTER TABLE shop_shipping_methods ADD COLUMN carrier_config jsonb;
24
+ 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;
25
+ ```
26
+
27
+ ### Notes
28
+
29
+ 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.
30
+
31
+ ## 0.15.2 — 2026-04-15
32
+
33
+ Shop: cena przechowywana jako numeric(20,6) — eliminacja driftu brutto/netto po reload; toggle netto/brutto per wariant.
34
+
35
+ ### Added
36
+ - Warianty produktu mają teraz toggle netto/brutto obok pola "Zmiana ceny" — spójne z toggle'em ceny bazowej. Delta zapisywana kanonicznie jako netto.
37
+
38
+ ### Fixed
39
+ - Shop: po wpisaniu ceny brutto (np. 65,00 zł przy VAT 23%) i refreshu, cena nie "ucieka" o ±1 grosz. Wartości przechowywane są jako PLN z 6 miejscami po przecinku (wzorzec PrestaShop), zamiast jednostek groszy — brutto zawsze odtwarzane z netto bez utraty informacji.
40
+ - Shipping: ta sama poprawka dla ceny metody wysyłki (stored jako netto PLN z 6dp).
41
+
42
+ ### Breaking
43
+ - Kolumny `shop_products.base_price`, `shop_product_variants.price_delta`, `shop_shipping_methods.price` zmieniły typ z `integer` (grosze) na `numeric(20,6)` (PLN). Snapshot zamówienia (`shop_orders.*`, `shop_order_items.price_*_snapshot`) pozostaje w groszach (`integer`) — KSeF-compatible.
44
+ - Publiczne typy `ShopDataWithVariants.basePrice`, `VariantRow.priceDelta`, `ShippingMethodRow.price` — wartość dalej `number`, ale wyrażona w PLN (nie groszach). Dla konsumentów SDK/populate: mnożenie × 100 jeśli potrzebujesz groszy.
45
+
46
+ ### Migration
47
+
48
+ ```sql
49
+ ALTER TABLE shop_products ALTER COLUMN base_price TYPE numeric(20,6) USING (base_price::numeric / 100);
50
+ ALTER TABLE shop_product_variants ALTER COLUMN price_delta TYPE numeric(20,6) USING (price_delta::numeric / 100);
51
+ ALTER TABLE shop_shipping_methods ALTER COLUMN price TYPE numeric(20,6) USING (price::numeric / 100);
52
+ ```
53
+
54
+ ### Notes
55
+
56
+ Migrację SQL trzeba uruchomić RAZ, PRZED `db:push` — dzieli istniejące wartości ÷100, bo stare dane były w groszach, a nowy typ przechowuje PLN. Po migracji `db:push` tylko zsynchronizuje schemat (bez zmiany danych).
57
+
6
58
  ## 0.15.1 — 2026-04-15
7
59
 
8
60
  Shop: PayU payment adapter + secure order access (token-gated view API, email link).
package/DOCS.md CHANGED
@@ -1,4 +1,4 @@
1
- # Includio CMS Documentation (v0.15.1)
1
+ # Includio CMS Documentation (v0.15.3)
2
2
 
3
3
  > This file is auto-generated from the docs site. For the latest version, update the package.
4
4
 
@@ -4311,6 +4311,7 @@ Headless e-commerce module. Optional — activate by adding `shop: defineShop({.
4311
4311
  - **Cart** — signed cookie, headless SDK (`createShopClient()`), `POST/PATCH/DELETE /api/shop/cart`.
4312
4312
  - **Checkout + orders** — `POST /api/shop/checkout` creates an order with consent validation, stock reservation (30-min TTL), and status emails.
4313
4313
  - **Payment adapters** — `manualAdapter()` (bank transfer / COD) and `payuAdapter()` ship built-in; plug your own by implementing `PaymentAdapter`.
4314
+ - **Carrier adapters** — `inpostAdapter()` ships built-in (Geowidget v5 picker + ShipX shipments + webhook). Plug other carriers by implementing `CarrierAdapter`.
4314
4315
  - **Webhook infrastructure** — `createPaymentWebhookHandler()` dispatches provider callbacks, verifies signatures, is idempotent.
4315
4316
  - **Secure order view** — per-order `accessToken` + cookie fallback + `GET /api/shop/orders/[number]` token-gated API.
4316
4317
  - **Shipping ↔ payment compatibility** — restrict payment methods per shipping (e.g. no COD for paczkomat).
@@ -4344,7 +4345,18 @@ export const cmsConfig = defineCMS({
4344
4345
 
4345
4346
  ## Routes
4346
4347
 
4347
- Scaffold provisions these endpoints in your app:
4348
+ 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.
4349
+
4350
+ 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:
4351
+
4352
+ ```sh
4353
+ pnpm includio scaffold admin # auto: detects shop in cms config
4354
+ pnpm includio scaffold admin --shop # force include shop routes
4355
+ pnpm includio scaffold admin --no-shop # force exclude shop routes
4356
+ pnpm includio scaffold admin --cms-config path/to/config.ts # custom location
4357
+ ```
4358
+
4359
+ Scaffold provisions these endpoints (when shop is enabled):
4348
4360
 
4349
4361
  | Path | Handler | Purpose |
4350
4362
  |------|---------|---------|
@@ -4355,11 +4367,15 @@ Scaffold provisions these endpoints in your app:
4355
4367
  | `POST /api/shop/orders/[number]/refresh-payment` | `createRefreshPaymentHandler()` | Pull status from provider |
4356
4368
  | `POST /api/shop/orders/[number]/retry-payment` | `createRetryPaymentHandler()` | New payment attempt |
4357
4369
  | `POST /api/shop/webhooks/[provider]` | `createPaymentWebhookHandler()` | Provider callbacks |
4370
+ | `GET /api/shop/carriers/[id]` | `createCarrierConfigHandler()` | Public carrier widget descriptor |
4371
+ | `POST /api/shop/carriers/[id]/webhook` | `createCarrierWebhookHandler()` | Carrier event receiver |
4372
+ | `GET /api/shop/admin/orders/[id]/label` | `createShipmentLabelHandler()` | Admin shipping-label PDF proxy |
4358
4373
 
4359
4374
  ## See also
4360
4375
 
4361
4376
  - [Order view](/docs/shop/order-view) — generic `<OrderStatus>` component + headless helper
4362
4377
  - [Retry payment](/docs/shop/retry-payment) — lifecycle + endpoint
4378
+ - [InPost carrier](/docs/shop/inpost) — Geowidget v5 + ShipX shipment + webhook
4363
4379
 
4364
4380
 
4365
4381
  ---
@@ -4527,6 +4543,125 @@ if (url) window.location.href = url;
4527
4543
  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
4544
 
4529
4545
 
4546
+ ---
4547
+
4548
+ # InPost carrier
4549
+
4550
+ `inpostAdapter()` ships in 0.15.3. Wires three things together:
4551
+
4552
+ 1. **Geowidget v5** — customer picks a paczkomat on the checkout page (`<InpostPicker>` Svelte component or raw config endpoint).
4553
+ 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.
4554
+ 3. **Webhook** — ShipX events (`confirmed`/`taken_by_courier`/`delivered`) update the order status and tracking number automatically.
4555
+
4556
+ > 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).
4557
+
4558
+ ## Config
4559
+
4560
+ ```ts
4561
+ import { defineShop, inpostAdapter } from 'includio-cms/shop';
4562
+
4563
+ defineShop({
4564
+ // …currency, payment, etc.
4565
+ carriers: [
4566
+ inpostAdapter({
4567
+ geowidgetToken: process.env.INPOST_GEOWIDGET_TOKEN!,
4568
+ shipxToken: process.env.INPOST_SHIPX_TOKEN!,
4569
+ organizationId: process.env.INPOST_ORG_ID!,
4570
+ environment: 'production', // or 'sandbox'
4571
+ webhookSecret: process.env.INPOST_WEBHOOK_SECRET!,
4572
+ senderAddress: {
4573
+ name: 'Twoja firma',
4574
+ company: 'AriaCMS',
4575
+ street: 'Mokotowska',
4576
+ buildingNumber: '1',
4577
+ city: 'Warszawa',
4578
+ postCode: '00-001',
4579
+ email: 'shop@example.com',
4580
+ phone: '+48500600700'
4581
+ }
4582
+ })
4583
+ ]
4584
+ });
4585
+ ```
4586
+
4587
+ **Optional opts:**
4588
+
4589
+ - `additionalServices: ['email', 'sms']` — InPost SMS/email notifications. Requires the service to be enabled on your account; sandbox usually doesn't have it.
4590
+ - `autoConfirm: false` — disable the post-create `POST /shipments/:id/buy` step. Default `true`.
4591
+ - `autoConfirmDelayMs` (default `1500`), `autoConfirmPollTimeoutMs` (default `8000`) — tune the offer-prep wait and post-buy polling.
4592
+ - `trackingUrlTemplate` — override `https://inpost.pl/sledzenie-przesylek?number={trackingNumber}`.
4593
+ - `labelFormat: 'Pdf' | 'ZebraLP'`, `labelSize: 'A4' | 'A6'` — default label format.
4594
+ - `debug: true` — verbose logging of the create→buy→poll flow (payload dump, every poll tick). Off by default; warnings + errors always log.
4595
+
4596
+ ## Routes scaffolded
4597
+
4598
+ `pnpm includio scaffold admin` provisions:
4599
+
4600
+ | Path | Handler | Purpose |
4601
+ |------|---------|---------|
4602
+ | `GET /api/shop/carriers/[id]` | `createCarrierConfigHandler()` | Public widget descriptor (script URL + token + preset) |
4603
+ | `POST /api/shop/carriers/[id]/webhook` | `createCarrierWebhookHandler()` | ShipX event receiver — secret-gated via `?secret=…` |
4604
+ | `GET /api/shop/admin/orders/[id]/label` | `createShipmentLabelHandler()` | Admin-only PDF proxy for the shipment label |
4605
+
4606
+ In `manager.paczkomaty.pl` configure the webhook URL as
4607
+ `https://your-domain.pl/api/shop/carriers/inpost/webhook?secret=<INPOST_WEBHOOK_SECRET>`.
4608
+
4609
+ ## Per-shipping-method service
4610
+
4611
+ Each shipping method with `carrierType=inpost` carries a `carrierConfig`:
4612
+
4613
+ | Field | Values |
4614
+ |------|--------|
4615
+ | `serviceType` | `inpost_locker_standard` (paczkomat), `inpost_locker_express`, `inpost_courier_standard`, `inpost_courier_express` |
4616
+ | `defaultSize` | `A` (small, 8×38×64 cm) / `B` (medium, 19×38×64 cm) / `C` (large, 41×38×64 cm) |
4617
+
4618
+ 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.
4619
+
4620
+ ## Frontend picker
4621
+
4622
+ ```svelte
4623
+ <InpostPicker
4624
+ bind:value={carrierRef}
4625
+ serviceType="inpost_locker_standard"
4626
+ onSelect={(point) => console.log('picked', point.name, point.address)}
4627
+ />
4628
+ ```
4629
+
4630
+ 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.
4631
+
4632
+ **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 } } }`.
4633
+
4634
+ ## Admin actions
4635
+
4636
+ On any order with an InPost shipping method, the order detail page shows:
4637
+
4638
+ - **"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`.
4639
+ - **"Pobierz etykietę PDF"** — opens the admin label proxy in a new tab.
4640
+ - **"Anuluj przesyłkę"** — `DELETE /v1/shipments/:id` on ShipX, clears local data. Only works after the shipment reaches `confirmed`.
4641
+
4642
+ ## Webhook → status mapping
4643
+
4644
+ | ShipX event status | Order status |
4645
+ |---|---|
4646
+ | `created`, `offers_prepared`, `offer_selected`, `confirmed`, `dispatched_by_sender` | `preparing` |
4647
+ | `taken_by_courier`, `adopted_at_*`, `out_for_delivery`, `ready_to_pickup` | `sent` |
4648
+ | `delivered` | `done` |
4649
+ | `canceled`, `returned_to_sender`, `rejected` | `cancelled` |
4650
+ | anything else | (ignored) |
4651
+
4652
+ 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`.
4653
+
4654
+ ## Sandbox quirks
4655
+
4656
+ - 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.
4657
+ - 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`.
4658
+ - Sandbox `additional_services: ['email', 'sms']` returns `unavailable` — drop the option for sandbox testing.
4659
+
4660
+ ## Customer tracking display
4661
+
4662
+ `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.
4663
+
4664
+
4530
4665
  ---
4531
4666
 
4532
4667
  # Migration Guide
package/ROADMAP.md CHANGED
@@ -304,10 +304,20 @@
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.2
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
+
310
+ ## 0.15.2 — Price precision fix
311
+
312
+ - [x] `[fix]` `[P1]` Price drift on reload — storage jako `numeric(20,6)` (PLN z 6dp), brutto round-tripuje bez utraty ±1gr. Snapshot zamówienia dalej w groszach (KSeF). <!-- files: src/lib/db-postgres/schema/shop/product.ts, productVariant.ts, shippingMethod.ts, src/lib/shop/pricing.ts, src/lib/shop/server/cart-hydrate.ts, shipping.ts, shop-data.ts, src/lib/admin/components/fields/shop-field.svelte, src/lib/admin/client/shop/shipping-method-form.svelte -->
313
+ - [x] `[feature]` `[P2]` Variant price toggle netto/brutto (spójne z toggle\'em ceny bazowej)
309
314
  - [x] `[feature]` `[P2]` `nodemailerAdapter` — typowanie `transportOptions` jako `SMTPTransport.Options` (OAuth2, pool, itd.)
310
315
 
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
+
311
321
  ## 0.16.0 — SEO module
312
322
 
313
323
  - [ ] `[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>;
@@ -10,10 +20,11 @@
10
20
  export interface ShippingFormPayload {
11
21
  name: Record<string, string>;
12
22
  description: Record<string, string> | null;
13
- price: number;
23
+ price: number; // PLN (number, netto)
14
24
  vatRate: number;
15
25
  carrierType: CarrierType;
16
- conditions: { freeAbove?: number } | null;
26
+ carrierConfig: CarrierConfig | null;
27
+ conditions: { freeAbove?: number } | null; // freeAbove: grosze
17
28
  allowedPaymentMethods: string[] | null;
18
29
  isActive: boolean;
19
30
  sortOrder: number | null;
@@ -21,9 +32,10 @@
21
32
  export interface ShippingFormInitial {
22
33
  name?: Record<string, string> | unknown;
23
34
  description?: Record<string, string> | unknown | null;
24
- price?: number;
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;
@@ -69,31 +81,40 @@
69
81
  )
70
82
  );
71
83
  type InputMode = 'net' | 'gross';
72
- let inputMode = $state<InputMode>(initial?.price != null ? 'net' : 'gross');
73
- let inputPrice = $state(initial?.price != null ? (initial.price / 100).toFixed(2) : '0.00');
84
+ // initial.price teraz PLN (number, netto) z API. Hydratujemy jako gross, żeby user widział dokładnie to co wpisał.
85
+ const initialNetPln = initial?.price != null ? Number(initial.price) : null;
86
+ const initialVat = Number(initial?.vatRate ?? vatRates[0] ?? 23);
87
+ let inputMode = $state<InputMode>(initialNetPln != null ? 'gross' : 'gross');
88
+ let inputPrice = $state(
89
+ initialNetPln != null
90
+ ? (initialNetPln * (1 + initialVat / 100)).toFixed(2)
91
+ : '0.00'
92
+ );
74
93
  let vatRate = $state<number | string>(initial?.vatRate ?? vatRates[0] ?? 23);
75
94
 
76
- const inputPriceCents = $derived(Math.round(parseFloat(inputPrice || '0') * 100));
95
+ const inputPln = $derived(parseFloat(inputPrice || '0') || 0);
77
96
  const vat = $derived(Number(vatRate) || 0);
78
- const netCents = $derived(
79
- inputMode === 'net' ? inputPriceCents : Math.round(inputPriceCents / (1 + vat / 100))
80
- );
81
- const grossCents = $derived(
82
- inputMode === 'gross' ? inputPriceCents : Math.round(inputPriceCents * (1 + vat / 100))
83
- );
84
- const vatCents = $derived(grossCents - netCents);
97
+ const netPln = $derived(inputMode === 'net' ? inputPln : inputPln / (1 + vat / 100));
98
+ const grossPln = $derived(inputMode === 'gross' ? inputPln : inputPln * (1 + vat / 100));
99
+ const vatPln = $derived(grossPln - netPln);
85
100
 
86
- function formatCents(cents: number) {
87
- return (cents / 100).toFixed(2);
101
+ function formatPln(pln: number) {
102
+ return pln.toFixed(2);
88
103
  }
89
104
 
90
105
  function switchMode(newMode: InputMode) {
91
106
  if (newMode === inputMode) return;
92
- const preservedCents = newMode === 'net' ? netCents : grossCents;
107
+ const preserved = newMode === 'net' ? netPln : grossPln;
93
108
  inputMode = newMode;
94
- inputPrice = formatCents(preservedCents);
109
+ inputPrice = formatPln(preserved);
95
110
  }
96
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
+ );
97
118
  let isActive = $state(initial?.isActive ?? true);
98
119
  // null/undefined = no restriction (all allowed); otherwise a whitelist.
99
120
  let restrictPayments = $state(
@@ -126,9 +147,13 @@
126
147
  await onsubmit({
127
148
  name: names,
128
149
  description: Object.values(descriptions).some((d) => d.length > 0) ? descriptions : null,
129
- price: netCents,
150
+ price: netPln,
130
151
  vatRate: Number(vatRate),
131
152
  carrierType,
153
+ carrierConfig:
154
+ carrierType === 'inpost'
155
+ ? { serviceType: inpostServiceType, defaultSize: inpostDefaultSize }
156
+ : null,
132
157
  conditions: freeAboveEnabled
133
158
  ? { freeAbove: Math.round(parseFloat(freeAbove || '0') * 100) }
134
159
  : null,
@@ -226,15 +251,15 @@
226
251
  <div class="bg-muted/40 border-border grid grid-cols-3 gap-2 rounded-lg border p-2.5 text-center text-xs">
227
252
  <div>
228
253
  <div class="text-muted-foreground font-semibold uppercase tracking-wide">Netto</div>
229
- <div class="text-sm font-bold tabular-nums">{formatCents(netCents)} zł</div>
254
+ <div class="text-sm font-bold tabular-nums">{formatPln(netPln)} zł</div>
230
255
  </div>
231
256
  <div class="border-border border-x">
232
257
  <div class="text-muted-foreground font-semibold uppercase tracking-wide">VAT</div>
233
- <div class="text-sm font-bold tabular-nums">{formatCents(vatCents)} zł</div>
258
+ <div class="text-sm font-bold tabular-nums">{formatPln(vatPln)} zł</div>
234
259
  </div>
235
260
  <div>
236
261
  <div class="text-muted-foreground font-semibold uppercase tracking-wide">Brutto</div>
237
- <div class="text-primary text-sm font-bold tabular-nums">{formatCents(grossCents)} zł</div>
262
+ <div class="text-primary text-sm font-bold tabular-nums">{formatPln(grossPln)} zł</div>
238
263
  </div>
239
264
  </div>
240
265
  <label class="flex items-center gap-2">
@@ -297,9 +322,52 @@
297
322
  <span class="mb-1 block text-sm font-semibold">Typ</span>
298
323
  <select bind:value={carrierType} class="border-border w-full rounded-lg border px-3 py-2">
299
324
  <option value="none">Bez integracji (adres)</option>
300
- <option value="inpost" disabled>InPost paczkomat (wkrótce)</option>
325
+ <option value="inpost">InPost</option>
301
326
  </select>
302
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}
303
371
  </section>
304
372
 
305
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;
@@ -19,9 +26,10 @@ export interface ShippingFormPayload {
19
26
  export interface ShippingFormInitial {
20
27
  name?: Record<string, string> | unknown;
21
28
  description?: Record<string, string> | unknown | null;
22
- price?: number;
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;
@@ -35,12 +35,15 @@
35
35
  }
36
36
  });
37
37
 
38
- function formatPrice(smallest: number) {
38
+ function formatPricePln(pln: number) {
39
39
  return new Intl.NumberFormat('pl-PL', {
40
40
  style: 'currency',
41
41
  currency: 'PLN',
42
42
  minimumFractionDigits: 2
43
- }).format(smallest / 100);
43
+ }).format(pln);
44
+ }
45
+ function formatPriceCents(cents: number) {
46
+ return formatPricePln(cents / 100);
44
47
  }
45
48
 
46
49
  async function doReorder(fromIndex: number, toIndex: number) {
@@ -143,11 +146,11 @@
143
146
  {resolveI18n(m.name as Record<string, string>, interfaceLanguage.current, '')}
144
147
  </a>
145
148
  </div>
146
- <div>{formatPrice(m.price)}</div>
149
+ <div>{formatPricePln(Number(m.price))}</div>
147
150
  <div>{m.vatRate}%</div>
148
151
  <div>
149
152
  {#if cond?.freeAbove != null}
150
- {formatPrice(cond.freeAbove)}
153
+ {formatPriceCents(cond.freeAbove)}
151
154
  {:else}
152
155
  <span class="text-muted-foreground text-xs">—</span>
153
156
  {/if}
@@ -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>
@@ -13,12 +13,12 @@
13
13
  const entriesQuery = $derived(remotes.listShopProductEntries());
14
14
  const collectionsQuery = $derived(remotes.listShopableCollections());
15
15
 
16
- function formatPrice(smallest: number) {
16
+ function formatPrice(pln: number) {
17
17
  return new Intl.NumberFormat('pl-PL', {
18
18
  style: 'currency',
19
19
  currency: 'PLN',
20
20
  minimumFractionDigits: 2
21
- }).format(smallest / 100);
21
+ }).format(pln);
22
22
  }
23
23
 
24
24
  function resolveTitle(data: Record<string, unknown> | null, fallback: string): string {