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
@@ -0,0 +1,16 @@
1
+ import type { CarrierEvent } from '../../types.js';
2
+ export declare class WebhookSecretMismatchError extends Error {
3
+ constructor();
4
+ }
5
+ export interface ParseWebhookOptions {
6
+ /** Expected secret. When provided, the URL must carry `?secret=...` matching it. */
7
+ secret?: string;
8
+ /** Override query parameter name (default `secret`). */
9
+ secretParam?: string;
10
+ }
11
+ /**
12
+ * Parse a ShipX webhook request into a `CarrierEvent`. When `secret` is set the
13
+ * URL is checked first — mismatch throws `WebhookSecretMismatchError` so the
14
+ * caller can return 401 without parsing the body.
15
+ */
16
+ export declare function parseShipxWebhook(req: Request, opts?: ParseWebhookOptions): Promise<CarrierEvent>;
@@ -0,0 +1,55 @@
1
+ import { mapShipxStatus } from './status-map.js';
2
+ /** Constant-time string compare, returns false on length mismatch. */
3
+ function safeEqual(a, b) {
4
+ if (a.length !== b.length)
5
+ return false;
6
+ let mismatch = 0;
7
+ for (let i = 0; i < a.length; i++) {
8
+ mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
9
+ }
10
+ return mismatch === 0;
11
+ }
12
+ export class WebhookSecretMismatchError extends Error {
13
+ constructor() {
14
+ super('Webhook secret mismatch.');
15
+ this.name = 'WebhookSecretMismatchError';
16
+ }
17
+ }
18
+ /**
19
+ * Parse a ShipX webhook request into a `CarrierEvent`. When `secret` is set the
20
+ * URL is checked first — mismatch throws `WebhookSecretMismatchError` so the
21
+ * caller can return 401 without parsing the body.
22
+ */
23
+ export async function parseShipxWebhook(req, opts = {}) {
24
+ if (opts.secret) {
25
+ const url = new URL(req.url);
26
+ const provided = url.searchParams.get(opts.secretParam ?? 'secret') ?? '';
27
+ if (!safeEqual(provided, opts.secret)) {
28
+ throw new WebhookSecretMismatchError();
29
+ }
30
+ }
31
+ const text = await req.text();
32
+ let body;
33
+ try {
34
+ body = JSON.parse(text);
35
+ }
36
+ catch {
37
+ throw new Error('Webhook body is not valid JSON.');
38
+ }
39
+ const payload = body.payload ?? {
40
+ shipment_id: body.shipment_id,
41
+ status: body.status,
42
+ tracking_number: body.tracking_number ?? null
43
+ };
44
+ const shipmentId = payload.shipment_id;
45
+ const status = payload.status ?? '';
46
+ if (shipmentId === undefined || shipmentId === null) {
47
+ throw new Error('Webhook payload missing shipment_id.');
48
+ }
49
+ return {
50
+ shipmentId: String(shipmentId),
51
+ trackingNumber: payload.tracking_number ?? undefined,
52
+ status: mapShipxStatus(status),
53
+ raw: body
54
+ };
55
+ }
@@ -12,6 +12,9 @@ export interface ShippingMethodPublic {
12
12
  price: number;
13
13
  vatRate: number;
14
14
  carrierType: string;
15
+ carrierConfig: {
16
+ serviceType: string | null;
17
+ } | null;
15
18
  conditions: {
16
19
  freeAbove?: number;
17
20
  } | null;
@@ -61,6 +64,8 @@ export interface OrderDetailResponse {
61
64
  paymentMethod: string | null;
62
65
  carrierType: string | null;
63
66
  carrierRef: string | null;
67
+ trackingNumber: string | null;
68
+ trackingUrl: string | null;
64
69
  language: string | null;
65
70
  createdAt: string;
66
71
  };
@@ -0,0 +1,12 @@
1
+ import { type RequestHandler } from '@sveltejs/kit';
2
+ /**
3
+ * Public endpoint that returns front-end facing carrier widget descriptor for
4
+ * a given carrier id (e.g. `inpost`). The descriptor never contains private
5
+ * credentials — only what the browser needs to render the picker (e.g. public
6
+ * geowidget token + script URL).
7
+ *
8
+ * Mounted at `/api/shop/carriers/[id]`.
9
+ */
10
+ export declare function createCarrierConfigHandler(): {
11
+ GET: RequestHandler;
12
+ };
@@ -0,0 +1,45 @@
1
+ import { json } from '@sveltejs/kit';
2
+ import { getCMS } from '../../core/cms.js';
3
+ function shopEnabled() {
4
+ try {
5
+ return getCMS().shopConfig !== null;
6
+ }
7
+ catch {
8
+ return false;
9
+ }
10
+ }
11
+ /**
12
+ * Public endpoint that returns front-end facing carrier widget descriptor for
13
+ * a given carrier id (e.g. `inpost`). The descriptor never contains private
14
+ * credentials — only what the browser needs to render the picker (e.g. public
15
+ * geowidget token + script URL).
16
+ *
17
+ * Mounted at `/api/shop/carriers/[id]`.
18
+ */
19
+ export function createCarrierConfigHandler() {
20
+ return {
21
+ GET: async ({ params }) => {
22
+ if (!shopEnabled())
23
+ return json({ error: 'Shop not enabled' }, { status: 404 });
24
+ const id = params.id;
25
+ if (!id)
26
+ return json({ error: 'Carrier id required' }, { status: 400 });
27
+ const shop = getCMS().shopConfig;
28
+ const carrier = shop?.carriers.find((c) => c.id === id);
29
+ if (!carrier)
30
+ return json({ error: 'Carrier not found' }, { status: 404 });
31
+ if (!carrier.widget) {
32
+ return json({ id: carrier.id, label: carrier.label ?? null, widget: null });
33
+ }
34
+ return json({
35
+ id: carrier.id,
36
+ label: carrier.label ?? null,
37
+ widget: {
38
+ scriptUrl: carrier.widget.scriptUrl ?? null,
39
+ stylesheetUrl: carrier.widget.stylesheetUrl ?? null,
40
+ config: carrier.widget.config
41
+ }
42
+ });
43
+ }
44
+ };
45
+ }
@@ -0,0 +1,13 @@
1
+ import { type RequestHandler } from '@sveltejs/kit';
2
+ /**
3
+ * Public webhook receiver for carrier events (e.g. ShipX `shipment_status_changed`).
4
+ * Carrier id (`inpost`, ...) is taken from the route param, looked up in
5
+ * `shop.carriers`, and the adapter's `handleWebhook` parses + verifies the
6
+ * request. Successful events are applied to the matching order (tracking +
7
+ * status). Unknown shipments still return 200 to avoid retry storms.
8
+ *
9
+ * Mounted at `POST /api/shop/carriers/[id]/webhook`.
10
+ */
11
+ export declare function createCarrierWebhookHandler(): {
12
+ POST: RequestHandler;
13
+ };
@@ -0,0 +1,66 @@
1
+ import { json } from '@sveltejs/kit';
2
+ import { getCMS } from '../../core/cms.js';
3
+ import { checkRateLimit, clientKey } from '../rate-limit.js';
4
+ import { applyCarrierEvent } from '../server/shipments.js';
5
+ function shopEnabled() {
6
+ try {
7
+ return getCMS().shopConfig !== null;
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }
13
+ /**
14
+ * Public webhook receiver for carrier events (e.g. ShipX `shipment_status_changed`).
15
+ * Carrier id (`inpost`, ...) is taken from the route param, looked up in
16
+ * `shop.carriers`, and the adapter's `handleWebhook` parses + verifies the
17
+ * request. Successful events are applied to the matching order (tracking +
18
+ * status). Unknown shipments still return 200 to avoid retry storms.
19
+ *
20
+ * Mounted at `POST /api/shop/carriers/[id]/webhook`.
21
+ */
22
+ export function createCarrierWebhookHandler() {
23
+ return {
24
+ POST: async ({ request, params }) => {
25
+ if (!shopEnabled())
26
+ return json({ error: 'Shop not enabled' }, { status: 404 });
27
+ const id = params.id;
28
+ if (!id)
29
+ return json({ error: 'Carrier id required' }, { status: 400 });
30
+ const shop = getCMS().shopConfig;
31
+ const adapter = shop?.carriers.find((c) => c.id === id);
32
+ if (!adapter)
33
+ return json({ error: 'Carrier not found' }, { status: 404 });
34
+ if (!adapter.handleWebhook) {
35
+ return json({ error: 'Carrier does not support webhooks' }, { status: 400 });
36
+ }
37
+ const rule = shop?.rateLimit.webhook ?? { limit: 60, windowSec: 60 };
38
+ const rl = checkRateLimit(clientKey(request, `webhook:${id}`), rule);
39
+ if (!rl.allowed) {
40
+ return json({ error: 'Rate limit exceeded' }, { status: 429 });
41
+ }
42
+ let event;
43
+ try {
44
+ event = await adapter.handleWebhook(request);
45
+ }
46
+ catch (err) {
47
+ const name = err instanceof Error ? err.name : '';
48
+ if (name === 'WebhookSecretMismatchError') {
49
+ return json({ error: 'Unauthorized' }, { status: 401 });
50
+ }
51
+ const message = err instanceof Error ? err.message : 'Webhook parse error';
52
+ console.error(`[shop/carrier:${id}] webhook parse failed:`, err);
53
+ return json({ error: message }, { status: 400 });
54
+ }
55
+ try {
56
+ const result = await applyCarrierEvent(event);
57
+ return json({ ok: true, ...result });
58
+ }
59
+ catch (err) {
60
+ console.error(`[shop/carrier:${id}] applyCarrierEvent failed:`, err);
61
+ const message = err instanceof Error ? err.message : 'Apply event failed';
62
+ return json({ error: message }, { status: 500 });
63
+ }
64
+ }
65
+ };
66
+ }
@@ -3,6 +3,7 @@ import { getCMS } from '../../core/cms.js';
3
3
  import { readCartCookie, writeCartCookie } from '../cart/cookie.js';
4
4
  import { writeOrderTokenCookie } from '../cart/order-token-cookie.js';
5
5
  import { createOrderFromCart, setPaymentProviderRef } from '../server/orders.js';
6
+ import { getShippingMethod } from '../server/shipping.js';
6
7
  import { checkRateLimit, clientKey } from '../rate-limit.js';
7
8
  import { requireShopConfig } from '../server/db.js';
8
9
  function shopEnabled() {
@@ -67,6 +68,27 @@ export function createCheckoutHandler() {
67
68
  if (cartItems.length === 0) {
68
69
  return json({ error: 'Cart is empty' }, { status: 400 });
69
70
  }
71
+ const carrierRef = asString(body.carrierRef, 200);
72
+ // Carrier-side validation (e.g. paczkomat code → InPost Points API)
73
+ try {
74
+ const shippingMethod = await getShippingMethod(shippingMethodId);
75
+ if (shippingMethod && shippingMethod.carrierType !== 'none') {
76
+ const shop = requireShopConfig();
77
+ const carrier = shop.carriers.find((c) => c.id === shippingMethod.carrierType);
78
+ if (carrier?.validateSelection) {
79
+ const ok = await carrier.validateSelection(carrierRef ?? '', {
80
+ serviceType: shippingMethod.carrierConfig?.serviceType
81
+ });
82
+ if (!ok) {
83
+ return json({ error: 'Invalid carrier selection (e.g. paczkomat unavailable).' }, { status: 400 });
84
+ }
85
+ }
86
+ }
87
+ }
88
+ catch (err) {
89
+ console.error('[shop] carrier validateSelection failed:', err);
90
+ return json({ error: 'Carrier validation failed.' }, { status: 400 });
91
+ }
70
92
  try {
71
93
  const result = await createOrderFromCart({
72
94
  cartItems,
@@ -75,7 +97,7 @@ export function createCheckoutHandler() {
75
97
  customerPhone: asString(body.customerPhone, 40),
76
98
  shippingAddress: asStringRecord(body.shippingAddress),
77
99
  shippingMethodId,
78
- carrierRef: asString(body.carrierRef, 200),
100
+ carrierRef,
79
101
  paymentMethod,
80
102
  consents: asConsents(body.consents),
81
103
  notes: asString(body.notes, 2000),
@@ -5,3 +5,6 @@ export { createOrderHandler } from './order-handler.js';
5
5
  export { createPaymentWebhookHandler } from './webhook-handler.js';
6
6
  export { createRefreshPaymentHandler } from './refresh-payment-handler.js';
7
7
  export { createRetryPaymentHandler } from './retry-payment-handler.js';
8
+ export { createCarrierConfigHandler } from './carrier-handler.js';
9
+ export { createCarrierWebhookHandler } from './carrier-webhook-handler.js';
10
+ export { createShipmentLabelHandler } from './shipment-label-handler.js';
@@ -5,3 +5,6 @@ export { createOrderHandler } from './order-handler.js';
5
5
  export { createPaymentWebhookHandler } from './webhook-handler.js';
6
6
  export { createRefreshPaymentHandler } from './refresh-payment-handler.js';
7
7
  export { createRetryPaymentHandler } from './retry-payment-handler.js';
8
+ export { createCarrierConfigHandler } from './carrier-handler.js';
9
+ export { createCarrierWebhookHandler } from './carrier-webhook-handler.js';
10
+ export { createShipmentLabelHandler } from './shipment-label-handler.js';
@@ -43,6 +43,18 @@ export function createOrderHandler() {
43
43
  getOrderItems(order.id),
44
44
  getOrderStatusHistory(order.id)
45
45
  ]);
46
+ let trackingUrl = null;
47
+ if (order.carrierType && order.trackingNumber) {
48
+ const adapter = getCMS().shopConfig?.carriers.find((c) => c.id === order.carrierType);
49
+ if (adapter?.trackingUrl) {
50
+ try {
51
+ trackingUrl = adapter.trackingUrl(order.trackingNumber);
52
+ }
53
+ catch {
54
+ trackingUrl = null;
55
+ }
56
+ }
57
+ }
46
58
  return json({
47
59
  order: {
48
60
  id: order.id,
@@ -61,6 +73,8 @@ export function createOrderHandler() {
61
73
  paymentMethod: order.paymentMethod,
62
74
  carrierType: order.carrierType,
63
75
  carrierRef: order.carrierRef,
76
+ trackingNumber: order.trackingNumber,
77
+ trackingUrl,
64
78
  language: order.language,
65
79
  createdAt: order.createdAt
66
80
  },
@@ -0,0 +1,10 @@
1
+ import { type RequestHandler } from '@sveltejs/kit';
2
+ /**
3
+ * Admin-only PDF proxy that streams the carrier label without exposing the
4
+ * carrier's signed link or token to the browser.
5
+ *
6
+ * Mounted at `/api/shop/admin/orders/[id]/label`.
7
+ */
8
+ export declare function createShipmentLabelHandler(): {
9
+ GET: RequestHandler;
10
+ };
@@ -0,0 +1,53 @@
1
+ import { error } from '@sveltejs/kit';
2
+ import { getCMS } from '../../core/cms.js';
3
+ import { requireAuth } from '../../admin/remote/middleware/auth.js';
4
+ import { getShipmentLabelForOrder } from '../server/shipments.js';
5
+ function shopEnabled() {
6
+ try {
7
+ return getCMS().shopConfig !== null;
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }
13
+ /**
14
+ * Admin-only PDF proxy that streams the carrier label without exposing the
15
+ * carrier's signed link or token to the browser.
16
+ *
17
+ * Mounted at `/api/shop/admin/orders/[id]/label`.
18
+ */
19
+ export function createShipmentLabelHandler() {
20
+ return {
21
+ GET: async ({ params, url }) => {
22
+ if (!shopEnabled())
23
+ error(404, 'Shop not enabled');
24
+ try {
25
+ requireAuth();
26
+ }
27
+ catch {
28
+ error(401, 'Unauthorized');
29
+ }
30
+ const orderId = params.id;
31
+ if (!orderId)
32
+ error(400, 'Order id required');
33
+ const sizeParam = url.searchParams.get('size');
34
+ const size = sizeParam === 'A4' || sizeParam === 'A6' ? sizeParam : undefined;
35
+ let label;
36
+ try {
37
+ label = await getShipmentLabelForOrder(orderId, { size });
38
+ }
39
+ catch (err) {
40
+ const message = err instanceof Error ? err.message : 'Label fetch failed';
41
+ error(400, message);
42
+ }
43
+ return new Response(new Uint8Array(label.body), {
44
+ status: 200,
45
+ headers: {
46
+ 'Content-Type': label.contentType,
47
+ 'Content-Disposition': `inline; filename="${label.filename ?? 'label.pdf'}"`,
48
+ 'Cache-Control': 'no-store'
49
+ }
50
+ });
51
+ }
52
+ };
53
+ }
@@ -23,6 +23,9 @@ export function createShippingMethodsHandler() {
23
23
  price: m.price,
24
24
  vatRate: m.vatRate,
25
25
  carrierType: m.carrierType,
26
+ carrierConfig: m.carrierConfig
27
+ ? { serviceType: m.carrierConfig.serviceType ?? null }
28
+ : null,
26
29
  conditions: m.conditions,
27
30
  allowedPaymentMethods: m.allowedPaymentMethods ?? null
28
31
  }))
@@ -3,4 +3,6 @@ export declare function defineShop(config: ShopConfig): ResolvedShopConfig;
3
3
  export { manualAdapter } from './adapters/manual/index.js';
4
4
  export { payuAdapter } from './adapters/payu/index.js';
5
5
  export type { PayuAdapterOptions } from './adapters/payu/index.js';
6
- export type { ShopConfig, ResolvedShopConfig, Currency, OrderStatus, PaymentAdapter, PaymentCreateContext, CarrierAdapter, ConsentConfig, ShopFeatures, PaymentCreateResult, PaymentEvent, OrderRef, I18nText } from './types.js';
6
+ export { inpostAdapter } from './adapters/inpost/index.js';
7
+ export type { InpostAdapterOptions, InpostSenderAddress, GeowidgetConfigPreset, InpostEnvironment } from './adapters/inpost/index.js';
8
+ export type { ShopConfig, ResolvedShopConfig, Currency, OrderStatus, PaymentAdapter, PaymentCreateContext, CarrierAdapter, CarrierEvent, ShipmentCreateInput, ShipmentCreateResult, ShipmentLabel, ConsentConfig, ShopFeatures, PaymentCreateResult, PaymentEvent, OrderRef, I18nText } from './types.js';
@@ -17,3 +17,4 @@ export function defineShop(config) {
17
17
  }
18
18
  export { manualAdapter } from './adapters/manual/index.js';
19
19
  export { payuAdapter } from './adapters/payu/index.js';
20
+ export { inpostAdapter } from './adapters/inpost/index.js';
@@ -3,6 +3,10 @@ import { resolveI18n } from '../pricing.js';
3
3
  import { getOrderById, getOrderItems } from './orders.js';
4
4
  import { requireShopConfig } from './db.js';
5
5
  import { buildOrderViewUrl } from './order-access-url.js';
6
+ const TRACKING_LABEL = {
7
+ pl: { label: 'Numer śledzenia', linkLabel: 'Sprawdź status przesyłki ↗' },
8
+ en: { label: 'Tracking number', linkLabel: 'Track your shipment ↗' }
9
+ };
6
10
  function formatPrice(cents, currency) {
7
11
  return new Intl.NumberFormat('pl-PL', {
8
12
  style: 'currency',
@@ -77,6 +81,15 @@ function renderHtml(ctx, intro) {
77
81
  <div style="font-size:16px;">Razem (brutto): <strong style="color:#5B4A9E;">${ctx.order.totalGross}</strong></div>
78
82
  <div style="color:#8888A0;font-size:12px;">netto ${ctx.order.totalNet} · VAT ${ctx.order.vatAmount}</div>
79
83
  </div>
84
+ ${ctx.tracking
85
+ ? `<div style="margin-top:20px;padding:16px;background:#F4F2FA;border-radius:10px;font-size:14px;">
86
+ <div style="color:#555566;margin-bottom:4px;">${escapeHtml(ctx.tracking.label)}</div>
87
+ <div style="font-family:ui-monospace,monospace;font-weight:700;word-break:break-all;">${escapeHtml(ctx.tracking.number)}</div>
88
+ ${ctx.tracking.url
89
+ ? `<a href="${escapeHtml(ctx.tracking.url)}" style="display:inline-block;margin-top:8px;color:#5B4A9E;font-weight:600;text-decoration:none;">${escapeHtml(ctx.tracking.linkLabel)}</a>`
90
+ : ''}
91
+ </div>`
92
+ : ''}
80
93
  ${ctx.viewUrl
81
94
  ? `<div style="margin-top:24px;text-align:center;"><a href="${escapeHtml(ctx.viewUrl)}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">${escapeHtml(ctx.viewLinkLabel)}</a></div>`
82
95
  : ''}
@@ -115,9 +128,33 @@ export async function sendOrderStatusEmail(orderId, status) {
115
128
  language: order.language
116
129
  })
117
130
  : null;
131
+ let tracking;
132
+ if (order.trackingNumber &&
133
+ (status === 'sent' || status === 'done' || status === 'preparing')) {
134
+ const carrier = order.carrierType
135
+ ? shop.carriers.find((c) => c.id === order.carrierType)
136
+ : undefined;
137
+ const url = carrier?.trackingUrl
138
+ ? (() => {
139
+ try {
140
+ return carrier.trackingUrl(order.trackingNumber);
141
+ }
142
+ catch {
143
+ return null;
144
+ }
145
+ })()
146
+ : null;
147
+ tracking = {
148
+ number: order.trackingNumber,
149
+ url,
150
+ label: TRACKING_LABEL[subjectKey].label,
151
+ linkLabel: TRACKING_LABEL[subjectKey].linkLabel
152
+ };
153
+ }
118
154
  const ctx = {
119
155
  viewUrl,
120
156
  viewLinkLabel: VIEW_LINK_LABEL[subjectKey],
157
+ tracking,
121
158
  order: {
122
159
  number: order.number,
123
160
  status: order.status,
@@ -33,6 +33,15 @@ export declare function updateOrderStatus(orderId: string, status: OrderStatus,
33
33
  changedBy?: string;
34
34
  }): Promise<OrderRow>;
35
35
  export declare function setPaymentProviderRef(orderId: string, ref: string | null): Promise<void>;
36
+ export interface ShipmentInfoInput {
37
+ shipmentId: string;
38
+ trackingNumber?: string | null;
39
+ labelUrl?: string | null;
40
+ }
41
+ export declare function setShipmentInfo(orderId: string, info: ShipmentInfoInput): Promise<OrderRow>;
42
+ export declare function updateTrackingNumber(orderId: string, trackingNumber: string): Promise<void>;
43
+ export declare function clearShipmentInfo(orderId: string): Promise<void>;
44
+ export declare function getOrderByShipmentId(shipmentId: string): Promise<OrderRow | null>;
36
45
  export declare function getOrderById(id: string): Promise<OrderRow | null>;
37
46
  export declare function getOrderByNumber(number: string): Promise<OrderRow | null>;
38
47
  export declare function getOrderItems(orderId: string): Promise<OrderItemRow[]>;
@@ -256,6 +256,54 @@ export async function setPaymentProviderRef(orderId, ref) {
256
256
  .set({ paymentProviderRef: ref, updatedAt: new Date() })
257
257
  .where(eq(shopOrdersTable.id, orderId));
258
258
  }
259
+ export async function setShipmentInfo(orderId, info) {
260
+ const db = getShopDb();
261
+ await db
262
+ .update(shopOrdersTable)
263
+ .set({
264
+ shipmentId: info.shipmentId,
265
+ trackingNumber: info.trackingNumber ?? null,
266
+ labelUrl: info.labelUrl ?? null,
267
+ shipmentCreatedAt: new Date(),
268
+ updatedAt: new Date()
269
+ })
270
+ .where(eq(shopOrdersTable.id, orderId));
271
+ const [row] = await db
272
+ .select()
273
+ .from(shopOrdersTable)
274
+ .where(eq(shopOrdersTable.id, orderId));
275
+ if (!row)
276
+ throw new Error('Order not found after shipment update.');
277
+ return row;
278
+ }
279
+ export async function updateTrackingNumber(orderId, trackingNumber) {
280
+ const db = getShopDb();
281
+ await db
282
+ .update(shopOrdersTable)
283
+ .set({ trackingNumber, updatedAt: new Date() })
284
+ .where(eq(shopOrdersTable.id, orderId));
285
+ }
286
+ export async function clearShipmentInfo(orderId) {
287
+ const db = getShopDb();
288
+ await db
289
+ .update(shopOrdersTable)
290
+ .set({
291
+ shipmentId: null,
292
+ trackingNumber: null,
293
+ labelUrl: null,
294
+ shipmentCreatedAt: null,
295
+ updatedAt: new Date()
296
+ })
297
+ .where(eq(shopOrdersTable.id, orderId));
298
+ }
299
+ export async function getOrderByShipmentId(shipmentId) {
300
+ const db = getShopDb();
301
+ const [row] = await db
302
+ .select()
303
+ .from(shopOrdersTable)
304
+ .where(eq(shopOrdersTable.shipmentId, shipmentId));
305
+ return row ?? null;
306
+ }
259
307
  export async function getOrderById(id) {
260
308
  const db = getShopDb();
261
309
  const [row] = await db.select().from(shopOrdersTable).where(eq(shopOrdersTable.id, id));
@@ -0,0 +1,33 @@
1
+ import type { CarrierEvent, ShipmentLabel } from '../types.js';
2
+ import { type OrderRow } from './orders.js';
3
+ /**
4
+ * Create a shipment for the order with the carrier configured on the order.
5
+ * Saves shipmentId/trackingNumber on the order and bumps status to `preparing`.
6
+ * Idempotent guard: throws if a shipment already exists (use `cancelShipmentForOrder` first).
7
+ */
8
+ export declare function createShipmentForOrder(orderId: string): Promise<OrderRow>;
9
+ /**
10
+ * Cancel an existing shipment with the carrier and clear shipment data on the order.
11
+ * Order status is left untouched (admin decides whether to drop back to `paid`).
12
+ */
13
+ export declare function cancelShipmentForOrder(orderId: string): Promise<OrderRow>;
14
+ /**
15
+ * Fetch the shipping label PDF (or other format) from the carrier. Used by the
16
+ * admin label proxy endpoint.
17
+ */
18
+ export declare function getShipmentLabelForOrder(orderId: string, opts?: {
19
+ size?: 'A4' | 'A6';
20
+ }): Promise<ShipmentLabel>;
21
+ export interface ApplyCarrierEventResult {
22
+ matched: boolean;
23
+ orderId?: string;
24
+ statusChanged?: boolean;
25
+ trackingChanged?: boolean;
26
+ }
27
+ /**
28
+ * Apply an incoming carrier event (e.g. from a webhook) to the matching order:
29
+ * update the tracking number when supplied and bump the order status when the
30
+ * mapped status differs from the current one. Unknown carrier statuses are
31
+ * skipped — caller still gets `matched=true` so the webhook can return 200.
32
+ */
33
+ export declare function applyCarrierEvent(event: CarrierEvent): Promise<ApplyCarrierEventResult>;