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.
- package/CHANGELOG.md +25 -0
- package/DOCS.md +137 -2
- package/ROADMAP.md +7 -2
- package/dist/admin/client/shop/shipping-method-form.svelte +66 -1
- package/dist/admin/client/shop/shipping-method-form.svelte.d.ts +8 -0
- package/dist/admin/client/shop/shop-order-detail-page.svelte +101 -0
- package/dist/admin/remote/shop.remote.d.ts +44 -0
- package/dist/admin/remote/shop.remote.js +35 -0
- package/dist/cli/index.js +49 -4
- package/dist/cli/scaffold/admin.d.ts +9 -2
- package/dist/cli/scaffold/admin.js +32 -3
- package/dist/db-postgres/schema/shop/order.d.ts +68 -0
- package/dist/db-postgres/schema/shop/order.js +4 -0
- package/dist/db-postgres/schema/shop/shippingMethod.d.ts +25 -0
- package/dist/db-postgres/schema/shop/shippingMethod.js +1 -0
- package/dist/shop/adapters/inpost/geowidget.d.ts +27 -0
- package/dist/shop/adapters/inpost/geowidget.js +31 -0
- package/dist/shop/adapters/inpost/index.d.ts +89 -0
- package/dist/shop/adapters/inpost/index.js +156 -0
- package/dist/shop/adapters/inpost/payload.d.ts +18 -0
- package/dist/shop/adapters/inpost/payload.js +85 -0
- package/dist/shop/adapters/inpost/points-api.d.ts +17 -0
- package/dist/shop/adapters/inpost/points-api.js +55 -0
- package/dist/shop/adapters/inpost/shipx-client.d.ts +56 -0
- package/dist/shop/adapters/inpost/shipx-client.js +95 -0
- package/dist/shop/adapters/inpost/status-map.d.ts +9 -0
- package/dist/shop/adapters/inpost/status-map.js +46 -0
- package/dist/shop/adapters/inpost/webhook.d.ts +16 -0
- package/dist/shop/adapters/inpost/webhook.js +55 -0
- package/dist/shop/client/index.d.ts +5 -0
- package/dist/shop/http/carrier-handler.d.ts +12 -0
- package/dist/shop/http/carrier-handler.js +45 -0
- package/dist/shop/http/carrier-webhook-handler.d.ts +13 -0
- package/dist/shop/http/carrier-webhook-handler.js +66 -0
- package/dist/shop/http/checkout-handler.js +23 -1
- package/dist/shop/http/index.d.ts +3 -0
- package/dist/shop/http/index.js +3 -0
- package/dist/shop/http/order-handler.js +14 -0
- package/dist/shop/http/shipment-label-handler.d.ts +10 -0
- package/dist/shop/http/shipment-label-handler.js +53 -0
- package/dist/shop/http/shipping-handler.js +3 -0
- package/dist/shop/index.d.ts +3 -1
- package/dist/shop/index.js +1 -0
- package/dist/shop/server/email.js +37 -0
- package/dist/shop/server/orders.d.ts +9 -0
- package/dist/shop/server/orders.js +48 -0
- package/dist/shop/server/shipments.d.ts +33 -0
- package/dist/shop/server/shipments.js +145 -0
- package/dist/shop/server/shipping.d.ts +2 -1
- package/dist/shop/server/shipping.js +9 -0
- package/dist/shop/svelte/InpostPicker.svelte +270 -0
- package/dist/shop/svelte/InpostPicker.svelte.d.ts +51 -0
- package/dist/shop/svelte/OrderStatus.svelte +53 -1
- package/dist/shop/svelte/index.d.ts +1 -0
- package/dist/shop/svelte/index.js +1 -0
- package/dist/shop/svelte/labels.d.ts +5 -0
- package/dist/shop/svelte/labels.js +6 -1
- package/dist/shop/types.d.ts +49 -1
- package/dist/updates/0.15.3/index.d.ts +2 -0
- package/dist/updates/0.15.3/index.js +19 -0
- package/dist/updates/index.js +2 -1
- 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
|
}))
|
package/dist/shop/index.d.ts
CHANGED
|
@@ -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
|
|
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';
|
package/dist/shop/index.js
CHANGED
|
@@ -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)
|