@myparcel-dev/pdk-admin 1.14.2 → 2.0.1

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 (107) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +6 -6
  3. package/src/__tests__/__snapshots__/exports.spec.ts.snap +8 -1
  4. package/src/__tests__/doComponentTestTeardown.ts +2 -0
  5. package/src/__tests__/mocks/index.ts +1 -1
  6. package/src/__tests__/mocks/mockDefaultAccount.ts +1 -0
  7. package/src/__tests__/mocks/mockDefaultGlobalContext.ts +2 -2
  8. package/src/__tests__/mocks/{mockDefaultPlatform.ts → mockDefaultProposition.ts} +1 -3
  9. package/src/__tests__/mocks/mockDefaultShop.ts +1 -0
  10. package/src/__tests__/utils/getActionIds.ts +12 -0
  11. package/src/__tests__/utils/mockOrderData.ts +20 -0
  12. package/src/actions/composables/mutations/orders/useExportOrdersMutation.ts +1 -1
  13. package/src/actions/composables/queries/account/useOrderCapabilitiesQuery.ts +67 -0
  14. package/src/actions/composables/queries/account/useShipmentCapabilitiesQuery.ts +96 -0
  15. package/src/actions/definitions/orders.ts +6 -8
  16. package/src/actions/index.ts +1 -0
  17. package/src/actions/orderModeActions.spec.ts +150 -0
  18. package/src/actions/orderModeActions.ts +90 -0
  19. package/src/components/OrderBox/ShipmentOptionsBox.spec.ts +109 -0
  20. package/src/components/OrderBox/ShipmentOptionsBox.vue +9 -25
  21. package/src/components/OrderListItem/OrderModeOrderListItem.spec.ts +125 -0
  22. package/src/components/OrderListItem/OrderModeOrderListItem.vue +10 -6
  23. package/src/components/PluginSettings/PluginSettingsForms.vue +7 -4
  24. package/src/components/common/DeliveryOptionsExcerptCarrierName.vue +3 -3
  25. package/src/components/modals/ShipmentOptionsModal.spec.ts +79 -0
  26. package/src/components/modals/ShipmentOptionsModal.vue +4 -21
  27. package/src/composables/components/useToggleInputContext.ts +2 -2
  28. package/src/composables/components/useTriStateInputContext.ts +2 -2
  29. package/src/composables/context/index.ts +1 -0
  30. package/src/composables/context/useOrderMode.spec.ts +75 -0
  31. package/src/composables/context/useOrderMode.ts +26 -0
  32. package/src/composables/language/useLanguage.spec.ts +2 -2
  33. package/src/composables/shipments/useShipmentData.ts +2 -2
  34. package/src/data/index.ts +1 -0
  35. package/src/data/orderMode.spec.ts +22 -0
  36. package/src/data/orderMode.ts +50 -0
  37. package/src/forms/form-builder/builders/buildAfterUpdate.spec.ts +5 -8
  38. package/src/forms/form-builder/operations/executeOperations.spec.ts +4 -7
  39. package/src/forms/form-builder/operations/executeSetPropOperation.spec.ts +4 -7
  40. package/src/forms/form-builder/operations/executeSetValueOperation.spec.ts +3 -2
  41. package/src/forms/form-builder/utils/createValueSetter.ts +21 -8
  42. package/src/forms/helpers/addCapabilitiesClearNotification.ts +39 -0
  43. package/src/forms/helpers/getDeliveryTypes.ts +5 -10
  44. package/src/forms/helpers/getFieldLabel.ts +7 -2
  45. package/src/forms/helpers/getPackageTypes.ts +7 -12
  46. package/src/forms/helpers/index.ts +3 -10
  47. package/src/forms/helpers/useFormCapabilities.spec.ts +227 -0
  48. package/src/forms/helpers/useFormCapabilities.ts +176 -0
  49. package/src/forms/pluginSettings/createPluginSettingsForm.ts +2 -2
  50. package/src/forms/pluginSettings/filterPluginSettingsView.spec.ts +158 -0
  51. package/src/forms/pluginSettings/filterPluginSettingsView.ts +96 -0
  52. package/src/forms/pluginSettings/index.ts +1 -0
  53. package/src/forms/pluginSettings/resolveSettingsValues.spec.ts +62 -0
  54. package/src/forms/pluginSettings/resolveSettingsValues.ts +16 -0
  55. package/src/forms/shipmentOptions/createShipmentOptionsForm.spec.ts +108 -0
  56. package/src/forms/shipmentOptions/createShipmentOptionsForm.ts +114 -52
  57. package/src/forms/shipmentOptions/field.ts +6 -66
  58. package/src/forms/shipmentOptions/fieldFactoryRegistry.ts +26 -0
  59. package/src/forms/shipmentOptions/fields/createCarrierField.ts +119 -30
  60. package/src/forms/shipmentOptions/fields/createDigitalStampRangeField.ts +3 -3
  61. package/src/forms/shipmentOptions/fields/createInsuranceField.ts +43 -22
  62. package/src/forms/shipmentOptions/fields/createPackageTypeField.ts +1 -2
  63. package/src/forms/shipmentOptions/fields/createShipmentOptionField.spec.ts +177 -0
  64. package/src/forms/shipmentOptions/fields/createShipmentOptionField.ts +90 -8
  65. package/src/forms/shipmentOptions/fields/index.ts +0 -9
  66. package/src/forms/shipmentOptions/useCapabilitiesAutoClear.spec.ts +347 -0
  67. package/src/forms/shipmentOptions/useCapabilitiesAutoClear.ts +452 -0
  68. package/src/forms/shipmentOptions/useCapabilitiesWatcher.spec.ts +14 -0
  69. package/src/forms/shipmentOptions/useCapabilitiesWatcher.ts +29 -0
  70. package/src/forms/shipmentOptions/wireProxyCapabilities.spec.ts +160 -0
  71. package/src/forms/shipmentOptions/wireProxyCapabilities.ts +102 -0
  72. package/src/index.ts +2 -0
  73. package/src/pdk/instance/plugins/createRegisterComponentsPlugin.ts +2 -0
  74. package/src/sdk/composables/api/useFetchCarrier.ts +39 -4
  75. package/src/sdk/composables/usePdkAdminApi.ts +8 -3
  76. package/src/stores/useActionStore.spec.ts +101 -0
  77. package/src/stores/useActionStore.ts +4 -28
  78. package/src/stores/useQueryStore.ts +15 -0
  79. package/src/types/actions/endpoints.types.ts +2 -1
  80. package/src/types/sdk.types.ts +54 -1
  81. package/src/utils/forms/formToBody.ts +2 -1
  82. package/src/utils/forms/getEnabledValues.spec.ts +66 -0
  83. package/src/utils/forms/getEnabledValues.ts +25 -0
  84. package/src/utils/forms/index.ts +1 -0
  85. package/src/utils/translations/getDynamicTranslation.ts +1 -1
  86. package/src/views/OrderBox.vue +11 -5
  87. package/src/views/OrderListItem.spec.ts +71 -0
  88. package/src/views/OrderListItem.vue +6 -7
  89. package/src/forms/helpers/createHasShipmentOptionWatcher.ts +0 -12
  90. package/src/forms/helpers/getCarrier.ts +0 -11
  91. package/src/forms/helpers/getInsuranceOptions.ts +0 -23
  92. package/src/forms/helpers/hasPostNlAgeCheck.ts +0 -11
  93. package/src/forms/helpers/hasShipmentOption.ts +0 -8
  94. package/src/forms/helpers/isPackageTypeMailbox.ts +0 -7
  95. package/src/forms/helpers/isPackageTypePackage.ts +0 -7
  96. package/src/forms/helpers/isPackageTypePackageOrSmall.ts +0 -7
  97. package/src/forms/helpers/isPackageTypeSmallPackage.ts +0 -7
  98. package/src/forms/helpers/setPostNlAgeCheckSubtext.ts +0 -17
  99. package/src/forms/shipmentOptions/fields/createAgeCheckField.ts +0 -33
  100. package/src/forms/shipmentOptions/fields/createDirectReturnField.ts +0 -8
  101. package/src/forms/shipmentOptions/fields/createHideSenderField.ts +0 -8
  102. package/src/forms/shipmentOptions/fields/createLargeFormatField.ts +0 -8
  103. package/src/forms/shipmentOptions/fields/createOnlyRecipientField.ts +0 -21
  104. package/src/forms/shipmentOptions/fields/createPriorityDeliveryField.ts +0 -12
  105. package/src/forms/shipmentOptions/fields/createReceiptCodeField.ts +0 -21
  106. package/src/forms/shipmentOptions/fields/createSameDayDeliveryField.ts +0 -8
  107. package/src/forms/shipmentOptions/fields/createSignatureField.ts +0 -11
@@ -1,7 +1,7 @@
1
1
  import {computed, type ComputedRef, type Ref, ref, toValue, unref} from 'vue';
2
2
  import {type MaybeRef} from '@vueuse/core';
3
- import {type Shipment} from '@myparcel-dev/pdk-common';
4
3
  import {type Carrier} from '@myparcel-dev/sdk';
4
+ import {type Shipment} from '@myparcel-dev/pdk-common';
5
5
  import {type AnyActionDefinition} from '../../types';
6
6
  import {useQueryStore} from '../../stores';
7
7
  import {instantiateActions} from '../../services';
@@ -32,7 +32,7 @@ export const useShipmentData = (id: MaybeRef<number>): UseShipmentData => {
32
32
  orderId ? queryStore.getQueriesForOrder(orderId) : undefined,
33
33
  ].filter(Boolean);
34
34
 
35
- const carrierName = shipment.value?.carrier?.name;
35
+ const carrierName = shipment.value?.carrier?.carrier;
36
36
  const carriersQuery = carrierName ? useFetchCarrier(carrierName) : undefined;
37
37
 
38
38
  return {
package/src/data/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './components';
2
2
  export * from './constants';
3
3
  export * from './endpoints';
4
+ export * from './orderMode';
4
5
  export * from './view';
@@ -0,0 +1,22 @@
1
+ import {describe, expect, it} from 'vitest';
2
+ import {OrderMode, resolveOrderMode, SubscriptionFeature} from './orderMode';
3
+
4
+ describe('resolveOrderMode', () => {
5
+ it('returns OrderV2 when both ORDER_MANAGEMENT and LEGACY_ORDER_MANAGEMENT are present', () => {
6
+ expect(resolveOrderMode([SubscriptionFeature.OrderManagement, SubscriptionFeature.LegacyOrderManagement])).toBe(
7
+ OrderMode.OrderV2,
8
+ );
9
+ });
10
+
11
+ it('returns OrderV2 when only ORDER_MANAGEMENT is present', () => {
12
+ expect(resolveOrderMode([SubscriptionFeature.OrderManagement])).toBe(OrderMode.OrderV2);
13
+ });
14
+
15
+ it('returns OrderV1 when only LEGACY_ORDER_MANAGEMENT is present', () => {
16
+ expect(resolveOrderMode([SubscriptionFeature.LegacyOrderManagement])).toBe(OrderMode.OrderV1);
17
+ });
18
+
19
+ it('returns Shipments when no features are present', () => {
20
+ expect(resolveOrderMode([])).toBe(OrderMode.Shipments);
21
+ });
22
+ });
@@ -0,0 +1,50 @@
1
+ export enum OrderMode {
2
+ Shipments = 'Shipments',
3
+ OrderV1 = 'OrderV1',
4
+ OrderV2 = 'OrderV2',
5
+ }
6
+
7
+ export enum SubscriptionFeature {
8
+ LegacyOrderManagement = 'LEGACY_ORDER_MANAGEMENT',
9
+ OrderManagement = 'ORDER_MANAGEMENT',
10
+ }
11
+
12
+ /**
13
+ * Mapping from the PHP-side `AccountFeaturesServiceInterface::ORDER_MODE_*`
14
+ * integer constants to the JS-side {@see OrderMode} enum.
15
+ */
16
+ const ORDER_MODE_BY_NUMERIC: Record<number, OrderMode> = {
17
+ 0: OrderMode.Shipments,
18
+ 1: OrderMode.OrderV1,
19
+ 2: OrderMode.OrderV2,
20
+ };
21
+
22
+ /**
23
+ * Translate the numeric `effectiveOrderMode` emitted by the PDK admin context
24
+ * (when present) into the JS-side {@see OrderMode} enum. Returns `undefined`
25
+ * when the value is missing or not one of the known modes, so callers can fall
26
+ * back to feature-based resolution.
27
+ */
28
+ export const orderModeFromContextValue = (value: unknown): OrderMode | undefined => {
29
+ if (typeof value === 'number' && value in ORDER_MODE_BY_NUMERIC) {
30
+ return ORDER_MODE_BY_NUMERIC[value];
31
+ }
32
+
33
+ return undefined;
34
+ };
35
+
36
+ /**
37
+ * Derive the active {@see OrderMode} from the raw IAM subscription features.
38
+ *
39
+ * Use this only as a fallback for environments where the PDK admin context does
40
+ * not yet surface a pre-computed `effectiveOrderMode`. Prefer the context value
41
+ * so any business rules layered on top in PHP (e.g. INT-1590's sales-channel
42
+ * downgrade) take effect on the JS side automatically.
43
+ */
44
+ export const resolveOrderMode = (features: string[]): OrderMode => {
45
+ if (features.includes(SubscriptionFeature.OrderManagement)) return OrderMode.OrderV2;
46
+
47
+ if (features.includes(SubscriptionFeature.LegacyOrderManagement)) return OrderMode.OrderV1;
48
+
49
+ return OrderMode.Shipments;
50
+ };
@@ -1,6 +1,6 @@
1
1
  import {ref} from 'vue';
2
2
  import {afterEach, describe, expect, it, vi} from 'vitest';
3
- import {mount} from '@vue/test-utils';
3
+ import {flushPromises, mount} from '@vue/test-utils';
4
4
  import {defineForm, MagicForm, useFormBuilder} from '@myparcel-dev/vue-form-builder';
5
5
  import {buildAfterUpdate} from './buildAfterUpdate';
6
6
 
@@ -31,19 +31,16 @@ describe('buildAfterUpdate', () => {
31
31
  ],
32
32
  });
33
33
 
34
- const wrapper = mount(MagicForm, {props: {form}});
34
+ mount(MagicForm, {props: {form}});
35
35
 
36
36
  expect(afterUpdate).not.toHaveBeenCalled();
37
37
 
38
38
  form.setValue('test', 'test');
39
39
 
40
- const testField = form.getField('test');
41
- // todo remove this when afterUpdate is properly triggered
42
- testField?.afterUpdate(testField);
40
+ // afterUpdate fires async, so wait for it to settle.
41
+ await flushPromises();
43
42
 
44
- await wrapper.vm.$nextTick();
45
-
46
- expect(afterUpdate).toHaveBeenCalledTimes(1);
43
+ expect(afterUpdate).toHaveBeenCalled();
47
44
  expect(form.getValue('test')).toBe('hello');
48
45
  });
49
46
  });
@@ -1,6 +1,6 @@
1
1
  import {ref} from 'vue';
2
2
  import {afterEach, describe, expect, it, vi} from 'vitest';
3
- import {mount} from '@vue/test-utils';
3
+ import {flushPromises, mount} from '@vue/test-utils';
4
4
  import {defineForm, MagicForm, useFormBuilder} from '@myparcel-dev/vue-form-builder';
5
5
  import {buildAfterUpdate} from '../builders';
6
6
 
@@ -49,17 +49,14 @@ describe('executeOperations', () => {
49
49
  ],
50
50
  });
51
51
 
52
- const wrapper = mount(MagicForm, {props: {form}});
52
+ mount(MagicForm, {props: {form}});
53
53
 
54
54
  expect(method).not.toHaveBeenCalled();
55
55
 
56
56
  form.setValue('test', 'test');
57
57
 
58
- const testField = form.getField('test');
59
- // todo remove this when afterUpdate is properly triggered
60
- testField?.afterUpdate(testField);
61
-
62
- await wrapper.vm.$nextTick();
58
+ // afterUpdate fires async, so wait for it to settle.
59
+ await flushPromises();
63
60
 
64
61
  expect(method).toHaveBeenCalledTimes(1);
65
62
  expect(method).toHaveBeenCalledWith({$value: 'hello'});
@@ -1,6 +1,6 @@
1
1
  import {ref} from 'vue';
2
2
  import {afterEach, describe, expect, it} from 'vitest';
3
- import {mount} from '@vue/test-utils';
3
+ import {flushPromises, mount} from '@vue/test-utils';
4
4
  import {defineForm, MagicForm, useFormBuilder} from '@myparcel-dev/vue-form-builder';
5
5
  import {type FormSetPropOperation, type PropVal} from '../types';
6
6
  import {buildAfterUpdate} from '../builders';
@@ -147,15 +147,12 @@ describe('executeSetPropOperation', () => {
147
147
  ],
148
148
  });
149
149
 
150
- const wrapper = mount(MagicForm, {props: {form}});
150
+ mount(MagicForm, {props: {form}});
151
151
 
152
152
  form.setValue('aardbei', 'test');
153
153
 
154
- const testField = form.getField('aardbei');
155
- // todo remove this when afterUpdate is properly triggered
156
- testField?.afterUpdate(testField);
157
-
158
- await wrapper.vm.$nextTick();
154
+ // afterUpdate fires async, so wait for it to settle.
155
+ await flushPromises();
159
156
 
160
157
  expect({
161
158
  aardbei: form.getField('aardbei')?.props,
@@ -1,8 +1,9 @@
1
1
  import {ref} from 'vue';
2
2
  import {afterEach, describe, expect, it} from 'vitest';
3
3
  import {flushPromises, mount} from '@vue/test-utils';
4
- import {defineForm, MagicForm, useFormBuilder} from '@myparcel-dev/vue-form-builder';
4
+ import {type InteractiveElementInstance, defineForm, MagicForm, useFormBuilder} from '@myparcel-dev/vue-form-builder';
5
5
  import {type AnyVal, type FormSetValueOperation} from '../types';
6
+ import {setFieldRef} from '../utils';
6
7
  import {buildAfterUpdate} from '../builders';
7
8
 
8
9
  interface TestInput {
@@ -192,7 +193,7 @@ describe('executeSetValueOperation', () => {
192
193
 
193
194
  mount(MagicForm, {props: {form}});
194
195
 
195
- form.setValue('test', 'test');
196
+ setFieldRef(form.getField('test') as InteractiveElementInstance, 'test');
196
197
 
197
198
  await flushPromises();
198
199
 
@@ -4,18 +4,31 @@ import {type AnyVal} from '../types';
4
4
 
5
5
  export type FormValueSetter = (value: AnyVal, target?: string) => void;
6
6
 
7
+ /**
8
+ * Set a field's ref value, handling both Vue Ref and raw value cases.
9
+ *
10
+ * vue-form-builder replaces `field.ref` with a raw value after the first
11
+ * render cycle, so we can't always rely on `field.ref.value = x`.
12
+ */
13
+ export const setFieldRef = (field: InteractiveElementInstance, value: AnyVal): void => {
14
+ if (isRef(field.ref)) {
15
+ field.ref.value = value;
16
+ } else {
17
+ // @ts-expect-error ref was replaced with a raw value by the component render
18
+ field.ref = value;
19
+ }
20
+ };
21
+
7
22
  export const createValueSetter = (instance: InteractiveElementInstance, prefix: string): FormValueSetter => {
8
23
  return (value, target) => {
9
24
  if (target) {
10
- instance.form.setValue(`${prefix}${target}`, value);
11
- } else {
12
- if (isRef(instance.ref)) {
13
- instance.ref.value = value;
14
- return;
15
- }
25
+ const targetField = instance.form.getField(`${prefix}${target}`) as InteractiveElementInstance | undefined;
16
26
 
17
- // @ts-expect-error todo
18
- instance.ref = value;
27
+ if (targetField) {
28
+ setFieldRef(targetField, value);
29
+ }
30
+ } else {
31
+ setFieldRef(instance, value);
19
32
  }
20
33
  };
21
34
  };
@@ -0,0 +1,39 @@
1
+ import {Variant} from '@myparcel-dev/pdk-common';
2
+ import {type PdkNotification} from '../../types';
3
+ import {useModalStore, type useNotificationStore} from '../../stores';
4
+ import {AdminModalKey, NotificationCategory} from '../../data';
5
+
6
+ export const CAPABILITIES_CLEARED_NOTIFICATION_ID = 'capabilities_cleared';
7
+
8
+ /**
9
+ * Add (or replace) the rolling "capabilities cleared" notification.
10
+ *
11
+ * The notification store skips `add` when an entry with the same id is already present, so
12
+ * we explicitly remove the prior one first to surface the new content list. Pass an
13
+ * already-localized `title` and `lines` — translation happens at the call site (auto-clear)
14
+ * because that's the layer holding the carrier name + axis context for parameterized lookups.
15
+ *
16
+ * Targets `NotificationCategory.Modal` when the shipment-options modal is open and
17
+ * `NotificationCategory.Action` otherwise — the modal shells (preset packages) only mount a
18
+ * `Modal` container, while the order-detail `ShipmentOptionsBox` mounts an `Action` container.
19
+ */
20
+ export const addCapabilitiesClearNotification = (
21
+ notificationStore: ReturnType<typeof useNotificationStore>,
22
+ title: string,
23
+ lines: string[],
24
+ ): void => {
25
+ if (lines.length === 0) return;
26
+
27
+ const isModal = AdminModalKey.ShipmentOptions === useModalStore().opened;
28
+
29
+ notificationStore.remove(CAPABILITIES_CLEARED_NOTIFICATION_ID);
30
+
31
+ notificationStore.add({
32
+ id: CAPABILITIES_CLEARED_NOTIFICATION_ID,
33
+ variant: Variant.Warning,
34
+ title,
35
+ content: lines,
36
+ category: isModal ? NotificationCategory.Modal : NotificationCategory.Action,
37
+ timeout: false,
38
+ } as PdkNotification);
39
+ };
@@ -1,18 +1,13 @@
1
- import {type FormInstance} from '@myparcel-dev/vue-form-builder';
2
- import {DeliveryTypeName} from '@myparcel-dev/constants';
1
+ import {type CarrierModel} from '@myparcel-dev/pdk-common';
3
2
  import {getDynamicTranslation} from '../../utils';
4
3
  import {type SelectOption} from '../../types';
5
- import {getCarrier} from './getCarrier';
6
4
 
7
- export const getDeliveryTypes = (form?: FormInstance): SelectOption[] => {
8
- let array = Object.values(DeliveryTypeName);
9
-
10
- if (form) {
11
- const carrier = getCarrier(form);
12
- array = array.filter((name) => carrier?.capabilities.deliveryTypes.includes(name));
5
+ export const getDeliveryTypes = (carrier: CarrierModel | undefined): SelectOption[] => {
6
+ if (!carrier) {
7
+ return [];
13
8
  }
14
9
 
15
- return array.map((name) => ({
10
+ return carrier.deliveryTypes.map((name) => ({
16
11
  label: getDynamicTranslation('delivery_type', name),
17
12
  value: name,
18
13
  }));
@@ -1,6 +1,11 @@
1
1
  import {snakeCase} from 'lodash-unified';
2
- import {type Shipment} from '@myparcel-dev/pdk-common';
3
2
  import {SHIPMENT_OPTIONS} from '../shipmentOptions';
4
3
 
5
- export const getFieldLabel = (name: keyof Shipment.ModelShipmentOptions): string =>
4
+ /**
5
+ * Generates a translation key for a shipment option field label.
6
+ *
7
+ * Accepts `string` because option names come from the carrier context at
8
+ * runtime. Converts to snake_case for the translation lookup.
9
+ */
10
+ export const getFieldLabel = (name: string): string =>
6
11
  snakeCase(`${SHIPMENT_OPTIONS}_${name}`);
@@ -1,19 +1,14 @@
1
- import {type FormInstance} from '@myparcel-dev/vue-form-builder';
2
- import {PackageTypeName} from '@myparcel-dev/constants';
1
+ import {type CarrierModel} from '@myparcel-dev/pdk-common';
3
2
  import {getPackageTypeTranslation} from '../../utils';
4
3
  import {type SelectOption} from '../../types';
5
- import {getCarrier} from './getCarrier';
6
4
 
7
- export const getPackageTypes = (form?: FormInstance): SelectOption[] => {
8
- let array = Object.values(PackageTypeName);
9
-
10
- if (form) {
11
- const carrier = getCarrier(form);
12
- array = array.filter((name) => carrier?.capabilities.packageTypes.includes(name));
5
+ export const getPackageTypes = (carrier: CarrierModel | undefined): SelectOption[] => {
6
+ if (!carrier) {
7
+ return [];
13
8
  }
14
9
 
15
- return array.map((name) => ({
16
- label: getPackageTypeTranslation(name),
17
- value: name,
10
+ return carrier.packageTypes.map((packageType) => ({
11
+ label: getPackageTypeTranslation(packageType),
12
+ value: packageType,
18
13
  }));
19
14
  };
@@ -1,21 +1,14 @@
1
1
  export * from './addBulkEditNotification';
2
+ export * from './addCapabilitiesClearNotification';
2
3
  export * from './createDefaultOption';
3
- export * from './createHasShipmentOptionWatcher';
4
4
  export * from './createLabel';
5
5
  export * from './defineFormField';
6
- export * from './getCarrier';
6
+ export * from './getDeliveryTypes';
7
7
  export * from './getFieldLabel';
8
8
  export * from './getFormCarrierName';
9
- export * from './getInsuranceOptions';
10
9
  export * from './getPackageTypes';
11
- export * from './hasPostNlAgeCheck';
12
- export * from './hasShipmentOption';
13
- export * from './isPackageTypeMailbox';
14
- export * from './isPackageTypePackage';
15
- export * from './isPackageTypePackageOrSmall';
16
- export * from './isPackageTypeSmallPackage';
17
10
  export * from './resolveFormComponent';
18
11
  export * from './setFieldProp';
19
- export * from './setPostNlAgeCheckSubtext';
20
12
  export * from './triStateFieldIsEnabled';
21
13
  export * from './updateFieldsDefaults';
14
+ export * from './useFormCapabilities';
@@ -0,0 +1,227 @@
1
+ import {describe, expect, it, vi, beforeEach} from 'vitest';
2
+ import {ref} from 'vue';
3
+ import {AdminContextKey, BackendEndpoint, type CarrierModel, TriState} from '@myparcel-dev/pdk-common';
4
+
5
+ type FakeQuery = {
6
+ status: ReturnType<typeof ref<string>>;
7
+ data: ReturnType<typeof ref<unknown>>;
8
+ };
9
+
10
+ const orderModifier = (id: string) => `${id}.order`;
11
+ const shipmentModifier = (id: string) => `${id}.shipment`;
12
+
13
+ const buildQueryStore = () => {
14
+ const queries = new Map<string, FakeQuery>();
15
+ const dynamicCarriers = ref<CarrierModel[]>([]);
16
+
17
+ return {
18
+ queries,
19
+ dynamicCarriers,
20
+ setQuery: (modifier: string, status: string, data: unknown) =>
21
+ queries.set(`${BackendEndpoint.ProxyCapabilities}.${modifier}`, {
22
+ status: ref(status),
23
+ data: ref(data),
24
+ }),
25
+ setDynamicCarriers: (carriers: CarrierModel[]) => {
26
+ dynamicCarriers.value = carriers;
27
+ },
28
+ has: (endpoint: BackendEndpoint, modifier: string) =>
29
+ queries.has(`${endpoint}.${modifier}`),
30
+ get: (endpoint: BackendEndpoint, modifier?: string) => {
31
+ if (endpoint === BackendEndpoint.FetchContext && modifier === AdminContextKey.Dynamic) {
32
+ return {
33
+ status: ref('success'),
34
+ data: ref({carriers: dynamicCarriers.value}),
35
+ };
36
+ }
37
+
38
+ return queries.get(`${endpoint}.${modifier}`) ?? {status: ref('idle'), data: ref(undefined)};
39
+ },
40
+ };
41
+ };
42
+
43
+ let queryStore: ReturnType<typeof buildQueryStore>;
44
+ let mockOrderId: string | undefined = 'order-1';
45
+
46
+ vi.mock('../../stores', () => ({
47
+ useQueryStore: () => queryStore,
48
+ }));
49
+
50
+ vi.mock('../../utils', () => ({
51
+ getOrderId: () => mockOrderId,
52
+ }));
53
+
54
+ vi.mock('../../composables', () => ({
55
+ Format: {Currency: 'currency'},
56
+ useContext: () => ({carriers: []}),
57
+ }));
58
+
59
+ const buildCarrier = (overrides: Partial<CarrierModel>): CarrierModel =>
60
+ ({
61
+ carrier: 'POSTNL',
62
+ packageTypes: [],
63
+ deliveryTypes: [],
64
+ options: {},
65
+ ...overrides,
66
+ }) as CarrierModel;
67
+
68
+ const formStub = (carrierName: string) => ({
69
+ getValue: (name: string) => (name === 'deliveryOptions.carrier' ? carrierName : undefined),
70
+ }) as never;
71
+
72
+ describe('useFormCapabilities', () => {
73
+ beforeEach(() => {
74
+ queryStore = buildQueryStore();
75
+ mockOrderId = 'order-1';
76
+ });
77
+
78
+ describe('getCarrierCapabilitiesForOrder', () => {
79
+ it('returns the order-query carrier when the query is success and the chosen carrier is in the results', async () => {
80
+ const order = buildCarrier({carrier: 'POSTNL', packageTypes: ['PACKAGE']});
81
+ const dynamic = buildCarrier({carrier: 'POSTNL', packageTypes: ['DIGITAL_STAMP']});
82
+ queryStore.setQuery(orderModifier('order-1'), 'success', [order]);
83
+ queryStore.setDynamicCarriers([dynamic]);
84
+
85
+ const {useFormCapabilities} = await import('./useFormCapabilities');
86
+ const capabilities = useFormCapabilities();
87
+
88
+ expect(capabilities.getCarrierCapabilitiesForOrder(formStub('POSTNL'))).toEqual(order);
89
+ });
90
+
91
+ it('falls back to dynamic carriers when the query is loading', async () => {
92
+ const dynamic = buildCarrier({carrier: 'POSTNL'});
93
+ queryStore.setQuery(orderModifier('order-1'), 'loading', undefined);
94
+ queryStore.setDynamicCarriers([dynamic]);
95
+
96
+ const {useFormCapabilities} = await import('./useFormCapabilities');
97
+
98
+ expect(useFormCapabilities().getCarrierCapabilitiesForOrder(formStub('POSTNL'))).toEqual(dynamic);
99
+ });
100
+
101
+ it('falls back to dynamic carriers when the query is errored (loading-state masquerade is intentional)', async () => {
102
+ const dynamic = buildCarrier({carrier: 'POSTNL'});
103
+ queryStore.setQuery(orderModifier('order-1'), 'error', undefined);
104
+ queryStore.setDynamicCarriers([dynamic]);
105
+
106
+ const {useFormCapabilities} = await import('./useFormCapabilities');
107
+
108
+ expect(useFormCapabilities().getCarrierCapabilitiesForOrder(formStub('POSTNL'))).toEqual(dynamic);
109
+ });
110
+
111
+ it('falls back to dynamic carriers when the orderId is missing (bulk path)', async () => {
112
+ mockOrderId = undefined;
113
+ const dynamic = buildCarrier({carrier: 'POSTNL'});
114
+ queryStore.setDynamicCarriers([dynamic]);
115
+
116
+ const {useFormCapabilities} = await import('./useFormCapabilities');
117
+
118
+ expect(useFormCapabilities().getCarrierCapabilitiesForOrder(formStub('POSTNL'))).toEqual(dynamic);
119
+ });
120
+ });
121
+
122
+ describe('getCarrierCapabilitiesForShipment tri-state', () => {
123
+ it('returns the narrow shipment carrier when the query is success and the carrier matches', async () => {
124
+ const shipment = buildCarrier({
125
+ carrier: 'POSTNL',
126
+ options: {requiresSignature: {isRequired: true} as never},
127
+ });
128
+ queryStore.setQuery(shipmentModifier('order-1'), 'success', [shipment]);
129
+
130
+ const {useFormCapabilities} = await import('./useFormCapabilities');
131
+
132
+ expect(useFormCapabilities().getCarrierCapabilitiesForShipment(formStub('POSTNL'))).toEqual(shipment);
133
+ });
134
+
135
+ it('falls back to order data when the shipment query is loading', async () => {
136
+ const order = buildCarrier({carrier: 'POSTNL', packageTypes: ['PACKAGE']});
137
+ queryStore.setQuery(orderModifier('order-1'), 'success', [order]);
138
+ queryStore.setQuery(shipmentModifier('order-1'), 'loading', undefined);
139
+
140
+ const {useFormCapabilities} = await import('./useFormCapabilities');
141
+
142
+ expect(useFormCapabilities().getCarrierCapabilitiesForShipment(formStub('POSTNL'))).toEqual(order);
143
+ });
144
+
145
+ it('returns undefined when the shipment query is success but the chosen carrier is not in the results (invalid combo)', async () => {
146
+ queryStore.setQuery(shipmentModifier('order-1'), 'success', [buildCarrier({carrier: 'DPD'})]);
147
+
148
+ const {useFormCapabilities} = await import('./useFormCapabilities');
149
+
150
+ expect(useFormCapabilities().getCarrierCapabilitiesForShipment(formStub('POSTNL'))).toBeUndefined();
151
+ });
152
+ });
153
+
154
+ describe('hasShipmentOption', () => {
155
+ it('returns true when the shipment carrier exposes the option', async () => {
156
+ const shipment = buildCarrier({
157
+ carrier: 'POSTNL',
158
+ options: {requiresSignature: {} as never},
159
+ });
160
+ queryStore.setQuery(shipmentModifier('order-1'), 'success', [shipment]);
161
+
162
+ const {useFormCapabilities} = await import('./useFormCapabilities');
163
+
164
+ expect(useFormCapabilities().hasShipmentOption(formStub('POSTNL'), 'requiresSignature')).toBe(true);
165
+ });
166
+
167
+ it('returns false when the option is absent from the carrier', async () => {
168
+ const shipment = buildCarrier({carrier: 'POSTNL', options: {}});
169
+ queryStore.setQuery(shipmentModifier('order-1'), 'success', [shipment]);
170
+
171
+ const {useFormCapabilities} = await import('./useFormCapabilities');
172
+
173
+ expect(useFormCapabilities().hasShipmentOption(formStub('POSTNL'), 'requiresSignature')).toBe(false);
174
+ });
175
+ });
176
+
177
+ describe('getInsuranceOptions', () => {
178
+ const formatter = {format: (_: unknown, n: number) => `€${n}`} as never;
179
+
180
+ it('returns [] when max insurance amount is zero', async () => {
181
+ const shipment = buildCarrier({
182
+ carrier: 'POSTNL',
183
+ options: {
184
+ insurance: {
185
+ insuredAmount: {
186
+ min: {amount: 0, currency: 'EUR'},
187
+ max: {amount: 0, currency: 'EUR'},
188
+ default: {amount: 0, currency: 'EUR'},
189
+ },
190
+ } as never,
191
+ },
192
+ });
193
+ queryStore.setQuery(shipmentModifier('order-1'), 'success', [shipment]);
194
+
195
+ const {useFormCapabilities} = await import('./useFormCapabilities');
196
+
197
+ expect(useFormCapabilities().getInsuranceOptions(formStub('POSTNL'), formatter)).toEqual([]);
198
+ });
199
+
200
+ it('switches step from 250 to 500 across the 500€ threshold', async () => {
201
+ const shipment = buildCarrier({
202
+ carrier: 'POSTNL',
203
+ options: {
204
+ insurance: {
205
+ insuredAmount: {
206
+ min: {amount: 0, currency: 'EUR'},
207
+ // max is 1000€ in cents
208
+ max: {amount: 100_000, currency: 'EUR'},
209
+ default: {amount: 0, currency: 'EUR'},
210
+ },
211
+ } as never,
212
+ },
213
+ });
214
+ queryStore.setQuery(shipmentModifier('order-1'), 'success', [shipment]);
215
+
216
+ const {useFormCapabilities} = await import('./useFormCapabilities');
217
+ const opts = useFormCapabilities().getInsuranceOptions(formStub('POSTNL'), formatter);
218
+ const values = opts.map((o) => o.value);
219
+
220
+ // First entry is the inherit default; subsequent values are cent amounts as strings.
221
+ expect(values[0]).toBe(TriState.Inherit);
222
+ // 0, 250, 500 (low steps) — then 1000 (high step from 500). Exact sequence verifies
223
+ // the boundary semantics — replacing `<` with `<=` on LOW_THRESHOLD would shift these.
224
+ expect(values.slice(1)).toEqual(['0', '25000', '50000', '100000']);
225
+ });
226
+ });
227
+ });