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
@@ -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>;
@@ -0,0 +1,145 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { shopShippingMethodsTable } from '../../db-postgres/schema/shop/index.js';
3
+ import { clearShipmentInfo, getOrderById, getOrderByShipmentId, setShipmentInfo, updateOrderStatus, updateTrackingNumber } from './orders.js';
4
+ import { getShopDb, requireShopConfig } from './db.js';
5
+ function findAdapterFor(order) {
6
+ if (!order.carrierType || order.carrierType === 'none') {
7
+ throw new Error('Order has no carrier configured.');
8
+ }
9
+ const shop = requireShopConfig();
10
+ const adapter = shop.carriers.find((c) => c.id === order.carrierType);
11
+ if (!adapter) {
12
+ throw new Error(`No carrier adapter registered for "${order.carrierType}". Add it to defineShop({ carriers: [...] }).`);
13
+ }
14
+ return adapter;
15
+ }
16
+ async function loadServiceConfig(shippingMethodId) {
17
+ if (!shippingMethodId) {
18
+ throw new Error('Order has no shipping method — cannot create shipment.');
19
+ }
20
+ const db = getShopDb();
21
+ const [row] = await db
22
+ .select()
23
+ .from(shopShippingMethodsTable)
24
+ .where(eq(shopShippingMethodsTable.id, shippingMethodId));
25
+ if (!row) {
26
+ throw new Error('Shipping method not found.');
27
+ }
28
+ const cfg = row.carrierConfig ?? {};
29
+ if (!cfg.serviceType) {
30
+ throw new Error('Shipping method has no carrierConfig.serviceType. Set it in admin → shipping methods.');
31
+ }
32
+ return { serviceType: cfg.serviceType, defaultSize: cfg.defaultSize };
33
+ }
34
+ /**
35
+ * Create a shipment for the order with the carrier configured on the order.
36
+ * Saves shipmentId/trackingNumber on the order and bumps status to `preparing`.
37
+ * Idempotent guard: throws if a shipment already exists (use `cancelShipmentForOrder` first).
38
+ */
39
+ export async function createShipmentForOrder(orderId) {
40
+ const order = await getOrderById(orderId);
41
+ if (!order)
42
+ throw new Error('Order not found.');
43
+ if (order.shipmentId) {
44
+ throw new Error(`Order already has a shipment (${order.shipmentId}). Cancel it first if you need to recreate.`);
45
+ }
46
+ const adapter = findAdapterFor(order);
47
+ if (!adapter.createShipment) {
48
+ throw new Error(`Carrier "${adapter.id}" does not support shipment creation.`);
49
+ }
50
+ const { serviceType, defaultSize } = await loadServiceConfig(order.shippingMethodId);
51
+ const result = await adapter.createShipment({
52
+ order: {
53
+ id: order.id,
54
+ number: order.number,
55
+ customerEmail: order.customerEmail,
56
+ customerName: order.customerName,
57
+ customerPhone: order.customerPhone
58
+ },
59
+ shippingAddress: order.shippingAddress,
60
+ carrierRef: order.carrierRef,
61
+ serviceType,
62
+ parcelSize: defaultSize,
63
+ cartTotalGross: order.totalGross,
64
+ language: order.language
65
+ });
66
+ await setShipmentInfo(order.id, {
67
+ shipmentId: result.shipmentId,
68
+ trackingNumber: result.trackingNumber || null,
69
+ labelUrl: result.labelUrl ?? null
70
+ });
71
+ if (order.status !== 'preparing' && order.status !== 'sent' && order.status !== 'done') {
72
+ await updateOrderStatus(order.id, 'preparing', {
73
+ note: `InPost shipment created (${result.shipmentId})`,
74
+ changedBy: 'admin'
75
+ });
76
+ }
77
+ const updated = await getOrderById(order.id);
78
+ if (!updated)
79
+ throw new Error('Order vanished after shipment update.');
80
+ return updated;
81
+ }
82
+ /**
83
+ * Cancel an existing shipment with the carrier and clear shipment data on the order.
84
+ * Order status is left untouched (admin decides whether to drop back to `paid`).
85
+ */
86
+ export async function cancelShipmentForOrder(orderId) {
87
+ const order = await getOrderById(orderId);
88
+ if (!order)
89
+ throw new Error('Order not found.');
90
+ if (!order.shipmentId) {
91
+ throw new Error('Order has no shipment to cancel.');
92
+ }
93
+ const adapter = findAdapterFor(order);
94
+ if (!adapter.cancelShipment) {
95
+ throw new Error(`Carrier "${adapter.id}" does not support shipment cancellation.`);
96
+ }
97
+ await adapter.cancelShipment(order.shipmentId);
98
+ await clearShipmentInfo(order.id);
99
+ const updated = await getOrderById(order.id);
100
+ if (!updated)
101
+ throw new Error('Order vanished after shipment cancel.');
102
+ return updated;
103
+ }
104
+ /**
105
+ * Fetch the shipping label PDF (or other format) from the carrier. Used by the
106
+ * admin label proxy endpoint.
107
+ */
108
+ export async function getShipmentLabelForOrder(orderId, opts = {}) {
109
+ const order = await getOrderById(orderId);
110
+ if (!order)
111
+ throw new Error('Order not found.');
112
+ if (!order.shipmentId)
113
+ throw new Error('Order has no shipment.');
114
+ const adapter = findAdapterFor(order);
115
+ if (!adapter.getShipmentLabel) {
116
+ throw new Error(`Carrier "${adapter.id}" does not support label fetching.`);
117
+ }
118
+ return adapter.getShipmentLabel(order.shipmentId, { size: opts.size });
119
+ }
120
+ /**
121
+ * Apply an incoming carrier event (e.g. from a webhook) to the matching order:
122
+ * update the tracking number when supplied and bump the order status when the
123
+ * mapped status differs from the current one. Unknown carrier statuses are
124
+ * skipped — caller still gets `matched=true` so the webhook can return 200.
125
+ */
126
+ export async function applyCarrierEvent(event) {
127
+ const order = await getOrderByShipmentId(event.shipmentId);
128
+ if (!order) {
129
+ return { matched: false };
130
+ }
131
+ let trackingChanged = false;
132
+ if (event.trackingNumber && event.trackingNumber !== order.trackingNumber) {
133
+ await updateTrackingNumber(order.id, event.trackingNumber);
134
+ trackingChanged = true;
135
+ }
136
+ let statusChanged = false;
137
+ if (event.status !== 'unknown' && event.status !== order.status) {
138
+ await updateOrderStatus(order.id, event.status, {
139
+ note: `Carrier event (${event.shipmentId})`,
140
+ changedBy: 'carrier-webhook'
141
+ });
142
+ statusChanged = true;
143
+ }
144
+ return { matched: true, orderId: order.id, statusChanged, trackingChanged };
145
+ }
@@ -1,5 +1,5 @@
1
1
  import { shopShippingMethodsTable } from '../../db-postgres/schema/shop/index.js';
2
- import type { ShopCarrierType } from '../../db-postgres/schema/shop/shippingMethod.js';
2
+ import type { ShippingCarrierConfig, ShopCarrierType } from '../../db-postgres/schema/shop/shippingMethod.js';
3
3
  type RawShippingMethodRow = typeof shopShippingMethodsTable.$inferSelect;
4
4
  export type ShippingMethodRow = Omit<RawShippingMethodRow, 'price'> & {
5
5
  price: number;
@@ -13,6 +13,7 @@ export interface ShippingMethodInput {
13
13
  price: number;
14
14
  vatRate: number;
15
15
  carrierType?: ShopCarrierType;
16
+ carrierConfig?: ShippingCarrierConfig | null;
16
17
  conditions?: ShippingConditions | null;
17
18
  allowedPaymentMethods?: string[] | null;
18
19
  isActive?: boolean;
@@ -21,6 +21,12 @@ function validate(input) {
21
21
  throw new Error('conditions.freeAbove must be non-negative integer (grosze).');
22
22
  }
23
23
  }
24
+ if (input.carrierType === 'inpost') {
25
+ const cfg = input.carrierConfig;
26
+ if (!cfg?.serviceType) {
27
+ throw new Error('InPost shipping method requires carrierConfig.serviceType.');
28
+ }
29
+ }
24
30
  }
25
31
  export async function listShippingMethods(opts = {}) {
26
32
  const db = getShopDb();
@@ -50,6 +56,7 @@ export async function createShippingMethod(input) {
50
56
  price: String(input.price),
51
57
  vatRate: input.vatRate,
52
58
  carrierType: input.carrierType ?? 'none',
59
+ carrierConfig: input.carrierConfig ?? null,
53
60
  conditions: input.conditions ?? null,
54
61
  allowedPaymentMethods: input.allowedPaymentMethods ?? null,
55
62
  isActive: input.isActive ?? true,
@@ -71,6 +78,8 @@ export async function updateShippingMethod(id, input) {
71
78
  patch.vatRate = input.vatRate;
72
79
  if (input.carrierType !== undefined)
73
80
  patch.carrierType = input.carrierType;
81
+ if (input.carrierConfig !== undefined)
82
+ patch.carrierConfig = input.carrierConfig;
74
83
  if (input.conditions !== undefined)
75
84
  patch.conditions = input.conditions;
76
85
  if (input.allowedPaymentMethods !== undefined)