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,31 @@
1
+ /**
2
+ * Geowidget v5 (Web Component) config used by `<InpostPicker>` on the frontend.
3
+ * The token is a public read-only "geowidget v5" token from your InPost account
4
+ * (https://manager.paczkomaty.pl). It must be delivered to the browser — do not
5
+ * pass private ShipX tokens here.
6
+ *
7
+ * Docs: https://dokumentacja-inpost.atlassian.net/wiki/spaces/PL/pages/50069505/Geowidget+v5
8
+ */
9
+ const GEOWIDGET_URLS = {
10
+ production: {
11
+ script: 'https://geowidget.inpost.pl/inpost-geowidget.js',
12
+ stylesheet: 'https://geowidget.inpost.pl/inpost-geowidget.css'
13
+ },
14
+ sandbox: {
15
+ script: 'https://sandbox-easy-geowidget-sdk.easypack24.net/inpost-geowidget.js',
16
+ stylesheet: 'https://sandbox-easy-geowidget-sdk.easypack24.net/inpost-geowidget.css'
17
+ }
18
+ };
19
+ export function buildGeowidgetDescriptor(opts) {
20
+ const env = opts.environment ?? 'production';
21
+ const urls = GEOWIDGET_URLS[env];
22
+ return {
23
+ scriptUrl: urls.script,
24
+ stylesheetUrl: urls.stylesheet,
25
+ config: {
26
+ token: opts.token,
27
+ language: opts.language ?? 'pl',
28
+ config: opts.config ?? 'parcelcollect'
29
+ }
30
+ };
31
+ }
@@ -0,0 +1,89 @@
1
+ import type { CarrierAdapter, I18nText } from '../../types.js';
2
+ import { type GeowidgetConfigPreset } from './geowidget.js';
3
+ import { type InpostEnvironment } from './points-api.js';
4
+ export interface InpostSenderAddress {
5
+ name: string;
6
+ company?: string;
7
+ street: string;
8
+ buildingNumber?: string;
9
+ city: string;
10
+ postCode: string;
11
+ countryCode?: string;
12
+ email: string;
13
+ phone: string;
14
+ }
15
+ export interface InpostAdapterOptions {
16
+ /**
17
+ * Public Geowidget v5 token (read-only). Delivered to the browser via
18
+ * `GET /api/shop/carriers/inpost`.
19
+ */
20
+ geowidgetToken: string;
21
+ /**
22
+ * Private ShipX organization-scoped token (`Bearer`). Used server-side for
23
+ * shipment creation, label fetching, cancellation. Optional in geowidget-only
24
+ * setups (Etap 2). Required for ShipX shipment operations (Etap 3).
25
+ */
26
+ shipxToken?: string;
27
+ /** ShipX organization ID, e.g. `123456`. Required when shipxToken is set. */
28
+ organizationId?: string;
29
+ /** `production` (default) or `sandbox`. Controls all ShipX endpoints. */
30
+ environment?: InpostEnvironment;
31
+ /**
32
+ * Shared secret used to authenticate webhook calls. Pass it as `?secret=`
33
+ * query in the `notifyUrl` you configure in the ShipX panel.
34
+ */
35
+ webhookSecret?: string;
36
+ /** Absolute URL ShipX should POST shipment events to. */
37
+ notifyUrl?: string;
38
+ /** Sender pickup address used for ShipX shipment creation. */
39
+ senderAddress?: InpostSenderAddress;
40
+ /**
41
+ * Tracking link template. `{trackingNumber}` is replaced. Default:
42
+ * `https://inpost.pl/sledzenie-przesylek?number={trackingNumber}`.
43
+ */
44
+ trackingUrlTemplate?: string;
45
+ /** Default geowidget preset. Default: `parcelCollect`. */
46
+ geowidgetPreset?: GeowidgetConfigPreset;
47
+ /** Default geowidget language. Default: `pl`. */
48
+ geowidgetLanguage?: string;
49
+ /** Cache TTL for Points API validation (ms). Default 5 min. */
50
+ pointsCacheTtlMs?: number;
51
+ /** Default ShipX label format (PDF). */
52
+ labelFormat?: 'Pdf' | 'ZebraLP';
53
+ /** Default ShipX label paper size. */
54
+ labelSize?: 'A4' | 'A6';
55
+ /** Additional ShipX services attached to every shipment (e.g. ['email', 'sms']). */
56
+ additionalServices?: string[];
57
+ /**
58
+ * After POST /shipments, automatically call POST /shipments/:id/buy to
59
+ * confirm the auto-selected offer. Required for accounts (esp. sandbox)
60
+ * where ShipX leaves shipments in `offer_selected` instead of paying
61
+ * automatically. Default `true`. Set `false` if your org is configured
62
+ * for full auto-pay or you handle confirmation yourself.
63
+ */
64
+ autoConfirm?: boolean;
65
+ /**
66
+ * Delay between POST /shipments and POST /shipments/:id/buy (ms). ShipX
67
+ * needs a brief moment to prepare the offer. Default 1500ms.
68
+ */
69
+ autoConfirmDelayMs?: number;
70
+ /**
71
+ * After POST /shipments/:id/buy, poll GET /shipments/:id every second
72
+ * until the status leaves `offer_selected` (becomes `confirmed` or further).
73
+ * ShipX buying is asynchronous — production is fast (~1s), sandbox can
74
+ * take minutes. Default 8000ms. Set 0 to disable polling and rely on
75
+ * the webhook to reconcile.
76
+ */
77
+ autoConfirmPollTimeoutMs?: number;
78
+ /**
79
+ * Verbose adapter logging — payload dumps, every poll tick, offer details.
80
+ * Off by default. Turn on when troubleshooting sandbox stalls or unexpected
81
+ * ShipX responses. Errors and the post-timeout stall warning always log.
82
+ */
83
+ debug?: boolean;
84
+ fetch?: typeof fetch;
85
+ id?: string;
86
+ label?: I18nText;
87
+ }
88
+ export declare function inpostAdapter(opts: InpostAdapterOptions): CarrierAdapter;
89
+ export type { GeowidgetConfigPreset, InpostEnvironment };
@@ -0,0 +1,156 @@
1
+ import { buildGeowidgetDescriptor } from './geowidget.js';
2
+ import { buildShipmentPayload } from './payload.js';
3
+ import { createPointsValidator } from './points-api.js';
4
+ import { ShipxClient } from './shipx-client.js';
5
+ import { parseShipxWebhook } from './webhook.js';
6
+ const DEFAULT_LABEL = { pl: 'InPost', en: 'InPost' };
7
+ const DEFAULT_TRACKING_TEMPLATE = 'https://inpost.pl/sledzenie-przesylek?number={trackingNumber}';
8
+ export function inpostAdapter(opts) {
9
+ if (!opts.geowidgetToken) {
10
+ throw new Error('inpostAdapter: `geowidgetToken` is required.');
11
+ }
12
+ const id = opts.id ?? 'inpost';
13
+ const env = opts.environment ?? 'production';
14
+ const fetchFn = opts.fetch ?? globalThis.fetch.bind(globalThis);
15
+ const points = createPointsValidator({
16
+ environment: env,
17
+ fetch: fetchFn,
18
+ cacheTtlMs: opts.pointsCacheTtlMs
19
+ });
20
+ const widgetDesc = buildGeowidgetDescriptor({
21
+ token: opts.geowidgetToken,
22
+ environment: env,
23
+ language: opts.geowidgetLanguage,
24
+ config: opts.geowidgetPreset
25
+ });
26
+ const trackingTemplate = opts.trackingUrlTemplate ?? DEFAULT_TRACKING_TEMPLATE;
27
+ function requireShipx() {
28
+ if (!opts.shipxToken) {
29
+ throw new Error('inpostAdapter: `shipxToken` is required for ShipX shipment operations.');
30
+ }
31
+ if (!opts.organizationId) {
32
+ throw new Error('inpostAdapter: `organizationId` is required for ShipX shipment operations.');
33
+ }
34
+ return new ShipxClient({
35
+ token: opts.shipxToken,
36
+ organizationId: opts.organizationId,
37
+ environment: env,
38
+ fetch: fetchFn
39
+ });
40
+ }
41
+ return {
42
+ id,
43
+ label: opts.label ?? DEFAULT_LABEL,
44
+ widget: {
45
+ scriptUrl: widgetDesc.scriptUrl,
46
+ stylesheetUrl: widgetDesc.stylesheetUrl,
47
+ config: { ...widgetDesc.config }
48
+ },
49
+ async validateSelection(ref, ctx) {
50
+ const serviceType = ctx?.serviceType;
51
+ // Courier service ships to a street address — no parcel locker code expected.
52
+ if (!serviceType || serviceType.startsWith('inpost_courier_'))
53
+ return true;
54
+ if (!ref)
55
+ return false;
56
+ return points.validate(ref);
57
+ },
58
+ async createShipment(input) {
59
+ const client = requireShipx();
60
+ const debug = opts.debug === true;
61
+ const log = (msg) => {
62
+ if (debug)
63
+ console.log(msg);
64
+ };
65
+ const payload = buildShipmentPayload({
66
+ input,
67
+ sender: opts.senderAddress,
68
+ additionalServices: opts.additionalServices
69
+ });
70
+ log(`[inpost] createShipment: order=${input.order.number} service=${input.serviceType} carrierRef=${input.carrierRef ?? '—'}`);
71
+ if (debug)
72
+ console.log('[inpost] payload →', JSON.stringify(payload, null, 2));
73
+ let shipment = await client.createShipment(payload);
74
+ log(`[inpost] POST /shipments → id=${shipment.id} status=${shipment.status} tracking=${shipment.tracking_number ?? 'null'}`);
75
+ if (opts.autoConfirm !== false) {
76
+ const delay = opts.autoConfirmDelayMs ?? 1500;
77
+ if (delay > 0) {
78
+ await new Promise((resolve) => setTimeout(resolve, delay));
79
+ }
80
+ try {
81
+ const fresh = await client.getShipment(shipment.id);
82
+ log(`[inpost] shipment now: status=${fresh.status} selected_offer=${fresh.selected_offer?.id ?? 'null'} offers=${fresh.offers?.length ?? 0}`);
83
+ const offerId = fresh.selected_offer?.id ??
84
+ (Array.isArray(fresh.offers) && fresh.offers.length > 0
85
+ ? fresh.offers[0].id
86
+ : undefined);
87
+ if (offerId == null) {
88
+ throw new Error(`Shipment ${shipment.id} has no selected_offer / offers yet (status=${fresh.status})`);
89
+ }
90
+ log(`[inpost] POST /shipments/${shipment.id}/buy {offer_id: ${offerId}}`);
91
+ const buyResult = await client.buyShipment(shipment.id, offerId);
92
+ // Polling: ShipX buy is asynchronous. Wait until we leave the
93
+ // offer_selected/created state, or give up and let the webhook reconcile.
94
+ const pollTimeout = opts.autoConfirmPollTimeoutMs ?? 8000;
95
+ const pollIntervalMs = 1000;
96
+ const deadline = Date.now() + pollTimeout;
97
+ shipment = buyResult;
98
+ while (pollTimeout > 0 &&
99
+ Date.now() < deadline &&
100
+ (shipment.status === 'offer_selected' || shipment.status === 'created')) {
101
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
102
+ shipment = await client.getShipment(shipment.id);
103
+ log(`[inpost] poll → status=${shipment.status} tracking=${shipment.tracking_number ?? 'null'}`);
104
+ }
105
+ log(`[inpost] final shipment: status=${shipment.status} tracking=${shipment.tracking_number ?? 'null'}`);
106
+ if (shipment.status === 'offer_selected' || shipment.status === 'created') {
107
+ console.warn(`[inpost] shipment ${shipment.id} still in '${shipment.status}' after ${pollTimeout}ms — sandbox often stalls here without a wallet/payment configured. Webhook will reconcile if/when ShipX completes the purchase.`);
108
+ }
109
+ }
110
+ catch (err) {
111
+ // Auto-confirm is best-effort. If the offer isn't ready, the
112
+ // account is configured for full auto-pay, or the offer was
113
+ // already bought, the webhook will reconcile the final status.
114
+ console.warn(`[inpost] auto-confirm failed for shipment ${shipment.id} — webhook will reconcile.`);
115
+ if (err instanceof Error) {
116
+ console.warn(`[inpost] ${err.name}: ${err.message}`);
117
+ const body = err.body;
118
+ if (body !== undefined) {
119
+ console.warn('[inpost] error body:', typeof body === 'string' ? body : JSON.stringify(body));
120
+ }
121
+ }
122
+ else {
123
+ console.warn('[inpost] auto-confirm error:', err);
124
+ }
125
+ }
126
+ }
127
+ return {
128
+ shipmentId: String(shipment.id),
129
+ trackingNumber: shipment.tracking_number ?? '',
130
+ raw: shipment
131
+ };
132
+ },
133
+ async getShipmentLabel(shipmentId, labelOpts) {
134
+ const client = requireShipx();
135
+ const { contentType, body } = await client.getLabel(shipmentId, {
136
+ format: opts.labelFormat ?? 'Pdf',
137
+ type: labelOpts?.size ?? opts.labelSize ?? 'A6'
138
+ });
139
+ return {
140
+ contentType,
141
+ body,
142
+ filename: `inpost-${shipmentId}.pdf`
143
+ };
144
+ },
145
+ async cancelShipment(shipmentId) {
146
+ const client = requireShipx();
147
+ await client.cancelShipment(shipmentId);
148
+ },
149
+ async handleWebhook(req) {
150
+ return parseShipxWebhook(req, { secret: opts.webhookSecret });
151
+ },
152
+ trackingUrl(trackingNumber) {
153
+ return trackingTemplate.replace('{trackingNumber}', encodeURIComponent(trackingNumber));
154
+ }
155
+ };
156
+ }
@@ -0,0 +1,18 @@
1
+ import type { ShipmentCreateInput } from '../../types.js';
2
+ import type { InpostSenderAddress } from './index.js';
3
+ export declare function mapParcelSize(size: string | undefined): string;
4
+ export declare function splitFullName(full: string | null | undefined): {
5
+ firstName?: string;
6
+ lastName?: string;
7
+ };
8
+ export interface BuildShipmentPayloadOptions {
9
+ input: ShipmentCreateInput;
10
+ sender?: InpostSenderAddress;
11
+ additionalServices?: string[];
12
+ }
13
+ /**
14
+ * Build a ShipX `POST /shipments` body for a given order. Locker services attach
15
+ * `custom_attributes.target_point` (paczkomat code from `carrierRef`); courier
16
+ * services use the receiver address from the order's shipping_address instead.
17
+ */
18
+ export declare function buildShipmentPayload(opts: BuildShipmentPayloadOptions): Record<string, unknown>;
@@ -0,0 +1,85 @@
1
+ const SIZE_TO_TEMPLATE = {
2
+ A: 'small',
3
+ B: 'medium',
4
+ C: 'large',
5
+ small: 'small',
6
+ medium: 'medium',
7
+ large: 'large'
8
+ };
9
+ export function mapParcelSize(size) {
10
+ if (!size)
11
+ return 'small';
12
+ return SIZE_TO_TEMPLATE[size] ?? 'small';
13
+ }
14
+ export function splitFullName(full) {
15
+ if (!full)
16
+ return {};
17
+ const trimmed = full.trim();
18
+ if (!trimmed)
19
+ return {};
20
+ const parts = trimmed.split(/\s+/);
21
+ if (parts.length === 1)
22
+ return { firstName: parts[0] };
23
+ return { firstName: parts[0], lastName: parts.slice(1).join(' ') };
24
+ }
25
+ /**
26
+ * Build a ShipX `POST /shipments` body for a given order. Locker services attach
27
+ * `custom_attributes.target_point` (paczkomat code from `carrierRef`); courier
28
+ * services use the receiver address from the order's shipping_address instead.
29
+ */
30
+ export function buildShipmentPayload(opts) {
31
+ const { input, sender, additionalServices } = opts;
32
+ const isLocker = input.serviceType.startsWith('inpost_locker_');
33
+ const isCourier = input.serviceType.startsWith('inpost_courier_');
34
+ const names = splitFullName(input.order.customerName);
35
+ const receiver = {
36
+ first_name: names.firstName ?? input.order.customerEmail,
37
+ last_name: names.lastName ?? '—',
38
+ email: input.order.customerEmail,
39
+ phone: input.order.customerPhone ?? undefined
40
+ };
41
+ if (isCourier) {
42
+ const addr = input.shippingAddress ?? {};
43
+ receiver.address = {
44
+ street: addr.street ?? addr.line1 ?? '',
45
+ building_number: addr.buildingNumber ?? addr.building_number ?? '',
46
+ city: addr.city ?? '',
47
+ post_code: addr.postCode ?? addr.post_code ?? addr.zip ?? '',
48
+ country_code: addr.countryCode ?? addr.country_code ?? 'PL'
49
+ };
50
+ }
51
+ const payload = {
52
+ receiver,
53
+ parcels: [{ template: mapParcelSize(input.parcelSize) }],
54
+ service: input.serviceType,
55
+ reference: input.order.number
56
+ };
57
+ if (isLocker && input.carrierRef) {
58
+ payload.custom_attributes = { target_point: input.carrierRef };
59
+ }
60
+ if (sender) {
61
+ payload.sender = {
62
+ name: sender.name,
63
+ company_name: sender.company,
64
+ email: sender.email,
65
+ phone: sender.phone,
66
+ address: {
67
+ street: sender.street,
68
+ building_number: sender.buildingNumber,
69
+ city: sender.city,
70
+ post_code: sender.postCode,
71
+ country_code: sender.countryCode ?? 'PL'
72
+ }
73
+ };
74
+ }
75
+ if (input.insuranceAmount && input.insuranceAmount > 0) {
76
+ payload.insurance = { amount: input.insuranceAmount, currency: 'PLN' };
77
+ }
78
+ if (input.cod && input.cod > 0) {
79
+ payload.cod = { amount: input.cod, currency: 'PLN' };
80
+ }
81
+ if (additionalServices && additionalServices.length > 0) {
82
+ payload.additional_services = additionalServices;
83
+ }
84
+ return payload;
85
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * InPost Points API — public, no auth required. Used to validate that a parcel
3
+ * locker code (e.g. "KRA01M") refers to a real, currently operating point.
4
+ *
5
+ * https://api-shipx-pl.easypack24.net/v1/points/{name}
6
+ */
7
+ export type InpostEnvironment = 'production' | 'sandbox';
8
+ export interface PointsApiOptions {
9
+ environment?: InpostEnvironment;
10
+ fetch?: typeof fetch;
11
+ cacheTtlMs?: number;
12
+ }
13
+ export interface PointsValidator {
14
+ validate(code: string): Promise<boolean>;
15
+ clearCache(): void;
16
+ }
17
+ export declare function createPointsValidator(opts?: PointsApiOptions): PointsValidator;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * InPost Points API — public, no auth required. Used to validate that a parcel
3
+ * locker code (e.g. "KRA01M") refers to a real, currently operating point.
4
+ *
5
+ * https://api-shipx-pl.easypack24.net/v1/points/{name}
6
+ */
7
+ const POINTS_BASE_URL = {
8
+ production: 'https://api-shipx-pl.easypack24.net',
9
+ sandbox: 'https://sandbox-api-shipx-pl.easypack24.net'
10
+ };
11
+ export function createPointsValidator(opts = {}) {
12
+ const env = opts.environment ?? 'production';
13
+ const fetchFn = opts.fetch ?? globalThis.fetch.bind(globalThis);
14
+ const cacheTtl = opts.cacheTtlMs ?? 5 * 60 * 1000;
15
+ const cache = new Map();
16
+ async function validate(code) {
17
+ const trimmed = code.trim();
18
+ if (!trimmed)
19
+ return false;
20
+ const now = Date.now();
21
+ const cached = cache.get(trimmed);
22
+ if (cached && cached.expiresAt > now)
23
+ return cached.value;
24
+ const url = `${POINTS_BASE_URL[env]}/v1/points/${encodeURIComponent(trimmed)}`;
25
+ let ok = false;
26
+ try {
27
+ const res = await fetchFn(url, {
28
+ method: 'GET',
29
+ headers: { Accept: 'application/json' }
30
+ });
31
+ if (res.status === 404) {
32
+ ok = false;
33
+ }
34
+ else if (res.ok) {
35
+ const data = (await res.json());
36
+ ok = data.status === 'Operating';
37
+ }
38
+ else {
39
+ // Transient/server error — fail open to avoid blocking checkout.
40
+ // Skip caching so a retry can succeed.
41
+ return true;
42
+ }
43
+ }
44
+ catch {
45
+ // Network failure — fail open. Don't cache.
46
+ return true;
47
+ }
48
+ cache.set(trimmed, { value: ok, expiresAt: now + cacheTtl });
49
+ return ok;
50
+ }
51
+ function clearCache() {
52
+ cache.clear();
53
+ }
54
+ return { validate, clearCache };
55
+ }
@@ -0,0 +1,56 @@
1
+ import type { InpostEnvironment } from './points-api.js';
2
+ export interface ShipxClientOptions {
3
+ token: string;
4
+ organizationId: string;
5
+ environment?: InpostEnvironment;
6
+ fetch?: typeof fetch;
7
+ }
8
+ export interface ShipxOffer {
9
+ id: number | string;
10
+ carrier_id?: string;
11
+ service_id?: string;
12
+ status?: string;
13
+ rate?: number;
14
+ [key: string]: unknown;
15
+ }
16
+ export interface ShipxShipment {
17
+ id: number | string;
18
+ status: string;
19
+ tracking_number?: string | null;
20
+ created_at?: string;
21
+ selected_offer?: ShipxOffer | null;
22
+ offers?: ShipxOffer[];
23
+ [key: string]: unknown;
24
+ }
25
+ export declare class ShipxApiError extends Error {
26
+ readonly status: number;
27
+ readonly body?: unknown | undefined;
28
+ constructor(message: string, status: number, body?: unknown | undefined);
29
+ }
30
+ export declare class ShipxClient {
31
+ private readonly base;
32
+ private readonly orgPath;
33
+ private readonly headers;
34
+ private readonly fetchFn;
35
+ constructor(opts: ShipxClientOptions);
36
+ createShipment(payload: Record<string, unknown>): Promise<ShipxShipment>;
37
+ getShipment(id: string | number): Promise<ShipxShipment>;
38
+ cancelShipment(id: string | number): Promise<void>;
39
+ /**
40
+ * Confirm/buy a specific offer for the shipment. Required when the account
41
+ * is not configured for fully-automatic offer payment (typical on sandbox).
42
+ * After this, status moves from `offer_selected` → `confirmed`.
43
+ *
44
+ * `offerId` is taken from `shipment.selected_offer.id` (or `offers[0].id`).
45
+ */
46
+ buyShipment(id: string | number, offerId: string | number): Promise<ShipxShipment>;
47
+ getLabel(id: string | number, opts?: {
48
+ format?: 'Pdf' | 'ZebraLP';
49
+ type?: 'A4' | 'A6';
50
+ }): Promise<{
51
+ contentType: string;
52
+ body: Uint8Array;
53
+ }>;
54
+ private request;
55
+ private requestRaw;
56
+ }
@@ -0,0 +1,95 @@
1
+ const SHIPX_BASE_URL = {
2
+ production: 'https://api-shipx-pl.easypack24.net',
3
+ sandbox: 'https://sandbox-api-shipx-pl.easypack24.net'
4
+ };
5
+ export class ShipxApiError extends Error {
6
+ status;
7
+ body;
8
+ constructor(message, status, body) {
9
+ super(message);
10
+ this.status = status;
11
+ this.body = body;
12
+ this.name = 'ShipxApiError';
13
+ }
14
+ }
15
+ export class ShipxClient {
16
+ base;
17
+ orgPath;
18
+ headers;
19
+ fetchFn;
20
+ constructor(opts) {
21
+ if (!opts.token)
22
+ throw new Error('ShipxClient: token required.');
23
+ if (!opts.organizationId)
24
+ throw new Error('ShipxClient: organizationId required.');
25
+ const env = opts.environment ?? 'production';
26
+ this.base = SHIPX_BASE_URL[env];
27
+ this.orgPath = `/v1/organizations/${encodeURIComponent(opts.organizationId)}`;
28
+ this.headers = {
29
+ Authorization: `Bearer ${opts.token}`,
30
+ 'Content-Type': 'application/json',
31
+ Accept: 'application/json'
32
+ };
33
+ this.fetchFn = opts.fetch ?? globalThis.fetch.bind(globalThis);
34
+ }
35
+ async createShipment(payload) {
36
+ return this.request('POST', `${this.orgPath}/shipments`, payload);
37
+ }
38
+ async getShipment(id) {
39
+ return this.request('GET', `/v1/shipments/${encodeURIComponent(String(id))}`);
40
+ }
41
+ async cancelShipment(id) {
42
+ await this.requestRaw('DELETE', `/v1/shipments/${encodeURIComponent(String(id))}`);
43
+ }
44
+ /**
45
+ * Confirm/buy a specific offer for the shipment. Required when the account
46
+ * is not configured for fully-automatic offer payment (typical on sandbox).
47
+ * After this, status moves from `offer_selected` → `confirmed`.
48
+ *
49
+ * `offerId` is taken from `shipment.selected_offer.id` (or `offers[0].id`).
50
+ */
51
+ async buyShipment(id, offerId) {
52
+ return this.request('POST', `/v1/shipments/${encodeURIComponent(String(id))}/buy`, { offer_id: typeof offerId === 'string' ? Number(offerId) : offerId });
53
+ }
54
+ async getLabel(id, opts = {}) {
55
+ const format = opts.format ?? 'Pdf';
56
+ const type = opts.type ?? 'A6';
57
+ const url = `${this.base}/v1/shipments/${encodeURIComponent(String(id))}/label?format=${format}&type=${type}`;
58
+ const res = await this.fetchFn(url, {
59
+ method: 'GET',
60
+ headers: { Authorization: this.headers.Authorization, Accept: 'application/pdf' }
61
+ });
62
+ if (!res.ok) {
63
+ const text = await res.text().catch(() => '');
64
+ throw new ShipxApiError(`ShipX label fetch failed (${res.status}): ${text || res.statusText}`, res.status, text);
65
+ }
66
+ const buf = new Uint8Array(await res.arrayBuffer());
67
+ return { contentType: res.headers.get('content-type') ?? 'application/pdf', body: buf };
68
+ }
69
+ async request(method, path, body) {
70
+ const res = await this.requestRaw(method, path, body);
71
+ if (res.status === 204)
72
+ return undefined;
73
+ const text = await res.text();
74
+ if (!text)
75
+ return undefined;
76
+ try {
77
+ return JSON.parse(text);
78
+ }
79
+ catch {
80
+ throw new ShipxApiError(`ShipX returned non-JSON response (${res.status})`, res.status, text);
81
+ }
82
+ }
83
+ async requestRaw(method, path, body) {
84
+ const res = await this.fetchFn(`${this.base}${path}`, {
85
+ method,
86
+ headers: this.headers,
87
+ body: body ? JSON.stringify(body) : undefined
88
+ });
89
+ if (!res.ok) {
90
+ const errBody = await res.text().catch(() => '');
91
+ throw new ShipxApiError(`ShipX ${method} ${path} failed (${res.status}): ${errBody || res.statusText}`, res.status, errBody);
92
+ }
93
+ return res;
94
+ }
95
+ }
@@ -0,0 +1,9 @@
1
+ import type { CarrierEvent } from '../../types.js';
2
+ /**
3
+ * Map a ShipX shipment status (from webhook payload or polling) to our domain
4
+ * OrderStatus. Returns `unknown` for transient/intermediate states the order
5
+ * lifecycle doesn't track — caller can ignore those.
6
+ *
7
+ * Reference: https://dokumentacja-inpost.atlassian.net/wiki/spaces/PL/pages/28639231
8
+ */
9
+ export declare function mapShipxStatus(status: string): CarrierEvent['status'];
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Map a ShipX shipment status (from webhook payload or polling) to our domain
3
+ * OrderStatus. Returns `unknown` for transient/intermediate states the order
4
+ * lifecycle doesn't track — caller can ignore those.
5
+ *
6
+ * Reference: https://dokumentacja-inpost.atlassian.net/wiki/spaces/PL/pages/28639231
7
+ */
8
+ export function mapShipxStatus(status) {
9
+ switch (status) {
10
+ case 'created':
11
+ case 'offers_prepared':
12
+ case 'offer_selected':
13
+ case 'confirmed':
14
+ case 'dispatched_by_sender_to_pok':
15
+ case 'dispatched_by_sender':
16
+ case 'collected_from_sender':
17
+ case 'taken_by_courier_from_pok':
18
+ return 'preparing';
19
+ case 'taken_by_courier':
20
+ case 'adopted_at_source_branch':
21
+ case 'sent_from_source_branch':
22
+ case 'adopted_at_sorting_center':
23
+ case 'sent_from_sorting_center':
24
+ case 'adopted_at_target_branch':
25
+ case 'out_for_delivery':
26
+ case 'ready_to_pickup':
27
+ case 'pickup_reminder_sent':
28
+ case 'pickup_time_expired':
29
+ case 'oversized':
30
+ case 'redirect_to_box':
31
+ return 'sent';
32
+ case 'delivered':
33
+ return 'done';
34
+ case 'canceled':
35
+ case 'cancelled':
36
+ case 'returned_to_sender':
37
+ case 'rejected':
38
+ case 'avizo':
39
+ case 'undelivered':
40
+ case 'unstored':
41
+ case 'claimed':
42
+ return 'cancelled';
43
+ default:
44
+ return 'unknown';
45
+ }
46
+ }