includio-cms 0.15.2 → 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 (62) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/DOCS.md +137 -2
  3. package/ROADMAP.md +7 -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 +32 -3
  12. package/dist/db-postgres/schema/shop/order.d.ts +68 -0
  13. package/dist/db-postgres/schema/shop/order.js +4 -0
  14. package/dist/db-postgres/schema/shop/shippingMethod.d.ts +25 -0
  15. package/dist/db-postgres/schema/shop/shippingMethod.js +1 -0
  16. package/dist/shop/adapters/inpost/geowidget.d.ts +27 -0
  17. package/dist/shop/adapters/inpost/geowidget.js +31 -0
  18. package/dist/shop/adapters/inpost/index.d.ts +89 -0
  19. package/dist/shop/adapters/inpost/index.js +156 -0
  20. package/dist/shop/adapters/inpost/payload.d.ts +18 -0
  21. package/dist/shop/adapters/inpost/payload.js +85 -0
  22. package/dist/shop/adapters/inpost/points-api.d.ts +17 -0
  23. package/dist/shop/adapters/inpost/points-api.js +55 -0
  24. package/dist/shop/adapters/inpost/shipx-client.d.ts +56 -0
  25. package/dist/shop/adapters/inpost/shipx-client.js +95 -0
  26. package/dist/shop/adapters/inpost/status-map.d.ts +9 -0
  27. package/dist/shop/adapters/inpost/status-map.js +46 -0
  28. package/dist/shop/adapters/inpost/webhook.d.ts +16 -0
  29. package/dist/shop/adapters/inpost/webhook.js +55 -0
  30. package/dist/shop/client/index.d.ts +5 -0
  31. package/dist/shop/http/carrier-handler.d.ts +12 -0
  32. package/dist/shop/http/carrier-handler.js +45 -0
  33. package/dist/shop/http/carrier-webhook-handler.d.ts +13 -0
  34. package/dist/shop/http/carrier-webhook-handler.js +66 -0
  35. package/dist/shop/http/checkout-handler.js +23 -1
  36. package/dist/shop/http/index.d.ts +3 -0
  37. package/dist/shop/http/index.js +3 -0
  38. package/dist/shop/http/order-handler.js +14 -0
  39. package/dist/shop/http/shipment-label-handler.d.ts +10 -0
  40. package/dist/shop/http/shipment-label-handler.js +53 -0
  41. package/dist/shop/http/shipping-handler.js +3 -0
  42. package/dist/shop/index.d.ts +3 -1
  43. package/dist/shop/index.js +1 -0
  44. package/dist/shop/server/email.js +37 -0
  45. package/dist/shop/server/orders.d.ts +9 -0
  46. package/dist/shop/server/orders.js +48 -0
  47. package/dist/shop/server/shipments.d.ts +33 -0
  48. package/dist/shop/server/shipments.js +145 -0
  49. package/dist/shop/server/shipping.d.ts +2 -1
  50. package/dist/shop/server/shipping.js +9 -0
  51. package/dist/shop/svelte/InpostPicker.svelte +270 -0
  52. package/dist/shop/svelte/InpostPicker.svelte.d.ts +51 -0
  53. package/dist/shop/svelte/OrderStatus.svelte +53 -1
  54. package/dist/shop/svelte/index.d.ts +1 -0
  55. package/dist/shop/svelte/index.js +1 -0
  56. package/dist/shop/svelte/labels.d.ts +5 -0
  57. package/dist/shop/svelte/labels.js +6 -1
  58. package/dist/shop/types.d.ts +49 -1
  59. package/dist/updates/0.15.3/index.d.ts +2 -0
  60. package/dist/updates/0.15.3/index.js +19 -0
  61. package/dist/updates/index.js +2 -1
  62. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,31 @@
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
+
6
31
  ## 0.15.2 — 2026-04-15
7
32
 
8
33
  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.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,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,11 @@
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
+
316
321
  ## 0.16.0 — SEO module
317
322
 
318
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>;
@@ -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();