@akinon/next 1.126.14-v1-rc.10 → 1.126.14

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 CHANGED
@@ -1,11 +1,10 @@
1
1
  # @akinon/next
2
2
 
3
- ## 1.126.14-v1-rc.10
3
+ ## 1.126.14
4
4
 
5
5
  ### Patch Changes
6
6
 
7
- - d480941: ZERO-4399: add card rewards to pz-masterpass-rest
8
- - 182a639b: ZERO-4483: Verify v1-rc maintenance pipeline (no-op patch to confirm v1-rc dist-tag flow)
7
+ - fb75f8e: ZERO-5168: Post the attribute-based shipping selection to the page name Commerce reports
9
8
 
10
9
  ## 1.126.13
11
10
 
@@ -0,0 +1,85 @@
1
+ const mockInitiate = jest.fn((options: Record<string, number>) => ({
2
+ type: 'mock/setAttributeBasedShippingOptions',
3
+ payload: options
4
+ }));
5
+
6
+ jest.mock('../data/client/checkout', () => ({
7
+ checkoutApi: {
8
+ endpoints: {
9
+ setAttributeBasedShippingOptions: {
10
+ initiate: (options: Record<string, number>) => mockInitiate(options)
11
+ }
12
+ }
13
+ }
14
+ }));
15
+
16
+ import { attributeBasedShippingOptionMiddleware } from '../redux/middlewares/pre-order/attribute-based-shipping-option';
17
+
18
+ const group = (...pks: number[]) => ({
19
+ attribute_based_shipping_options: pks.map((pk) => ({ pk })),
20
+ product_ids: [1]
21
+ });
22
+
23
+ const run = (attributeBasedShippingOptions: Record<string, unknown>) => {
24
+ const dispatch = jest.fn();
25
+ const action = { payload: { pre_order: {} } };
26
+ const next = jest.fn(() => action);
27
+
28
+ const result = (attributeBasedShippingOptionMiddleware as any)({
29
+ getState: () => ({ checkout: { attributeBasedShippingOptions } }),
30
+ dispatch
31
+ })(next)(action);
32
+
33
+ return { dispatch, result, action };
34
+ };
35
+
36
+ describe('attributeBasedShippingOptionMiddleware', () => {
37
+ beforeEach(() => {
38
+ mockInitiate.mockClear();
39
+ });
40
+
41
+ it('selects the first option of every group', () => {
42
+ const { dispatch, result } = run({
43
+ express: group(11, 12),
44
+ standart: group(21)
45
+ });
46
+
47
+ expect(mockInitiate).toHaveBeenCalledWith({ express: 11, standart: 21 });
48
+ expect(dispatch).toHaveBeenCalledTimes(1);
49
+ expect(result).toBeNull();
50
+ });
51
+
52
+ it('posts nothing when only some of the reported groups are selectable', () => {
53
+ // The backend rejects a selection that does not cover every group it
54
+ // reported, and that rejection leaves pre_order empty, which re-arms this
55
+ // middleware. Posting a partial map would loop.
56
+ const { dispatch, result, action } = run({
57
+ express: group(11),
58
+ None: group()
59
+ });
60
+
61
+ expect(mockInitiate).not.toHaveBeenCalled();
62
+ expect(dispatch).not.toHaveBeenCalled();
63
+ expect(result).toBe(action);
64
+ });
65
+
66
+ it('does not dispatch when no group has a selectable option at all', () => {
67
+ const { dispatch, result, action } = run({ None: group() });
68
+
69
+ expect(mockInitiate).not.toHaveBeenCalled();
70
+ expect(dispatch).not.toHaveBeenCalled();
71
+ expect(result).toBe(action);
72
+ });
73
+
74
+ it('leaves a malformed group alone instead of throwing', () => {
75
+ expect(() => run({ None: {} })).not.toThrow();
76
+ expect(mockInitiate).not.toHaveBeenCalled();
77
+ });
78
+
79
+ it('still posts when every reported group is selectable', () => {
80
+ const { dispatch } = run({ express: group(11), standart: group(21, 22) });
81
+
82
+ expect(mockInitiate).toHaveBeenCalledWith({ express: 11, standart: 21 });
83
+ expect(dispatch).toHaveBeenCalledTimes(1);
84
+ });
85
+ });
@@ -0,0 +1,48 @@
1
+ import {
2
+ getAttributeBasedShippingOptionUrl,
3
+ setAttributeBasedShippingPageName
4
+ } from '../data/client/checkout';
5
+ import { checkout } from '../data/urls';
6
+
7
+ describe('attribute-based shipping option url', () => {
8
+ afterEach(() => {
9
+ setAttributeBasedShippingPageName(null);
10
+ });
11
+
12
+ it('falls back to the classic page when nothing has been reported', () => {
13
+ expect(getAttributeBasedShippingOptionUrl()).toBe(
14
+ checkout.setAttributeBasedShippingOption
15
+ );
16
+ });
17
+
18
+ it('posts to the page name the backend reported for the step', () => {
19
+ setAttributeBasedShippingPageName(
20
+ 'RemoteAttributeBasedShippingOptionSelectionPage'
21
+ );
22
+
23
+ expect(getAttributeBasedShippingOptionUrl()).toBe(
24
+ '/orders/checkout/?page=RemoteAttributeBasedShippingOptionSelectionPage'
25
+ );
26
+ });
27
+
28
+ it('keeps the classic url when the reported name is the classic page', () => {
29
+ setAttributeBasedShippingPageName(
30
+ 'AttributeBasedShippingOptionSelectionPage'
31
+ );
32
+
33
+ expect(getAttributeBasedShippingOptionUrl()).toBe(
34
+ checkout.setAttributeBasedShippingOption
35
+ );
36
+ });
37
+
38
+ it('returns to the fallback once the reported name is cleared', () => {
39
+ setAttributeBasedShippingPageName(
40
+ 'RemoteAttributeBasedShippingOptionSelectionPage'
41
+ );
42
+ setAttributeBasedShippingPageName(undefined);
43
+
44
+ expect(getAttributeBasedShippingOptionUrl()).toBe(
45
+ checkout.setAttributeBasedShippingOption
46
+ );
47
+ });
48
+ });
@@ -145,6 +145,25 @@ const buildCheckoutRequestUrl = (
145
145
  return buildClientRequestUrl(effectivePath, options);
146
146
  };
147
147
 
148
+ // Commerce serves the attribute-based shipping step under more than one page
149
+ // name: the classic page and the remote variant, chosen by a dynamic setting we
150
+ // cannot see from here. Remember the page name reported for that step so the
151
+ // selection is posted back to the same page. contextListMiddleware is the only
152
+ // place that sees context_list, so it writes the value here; an RTK Query
153
+ // `query` callback is synchronous and cannot reach the store.
154
+ let attributeBasedShippingPageName: string | null = null;
155
+
156
+ export const setAttributeBasedShippingPageName = (pageName?: string | null) => {
157
+ attributeBasedShippingPageName = pageName ?? null;
158
+ };
159
+
160
+ export const getAttributeBasedShippingOptionUrl = () =>
161
+ attributeBasedShippingPageName
162
+ ? checkout.setAttributeBasedShippingOptionPage(
163
+ attributeBasedShippingPageName
164
+ )
165
+ : checkout.setAttributeBasedShippingOption;
166
+
148
167
  const completeMasterpassPayment = async (
149
168
  params: CompleteCreditCardParams,
150
169
  dispatch: AppDispatch,
@@ -1003,7 +1022,7 @@ export const checkoutApi = api.injectEndpoints({
1003
1022
  Record<string, number>
1004
1023
  >({
1005
1024
  query: (options) => ({
1006
- url: buildCheckoutRequestUrl(checkout.setAttributeBasedShippingOption, {
1025
+ url: buildCheckoutRequestUrl(getAttributeBasedShippingOptionUrl(), {
1007
1026
  useFormData: true
1008
1027
  }),
1009
1028
  method: 'POST',
@@ -1013,8 +1032,11 @@ export const checkoutApi = api.injectEndpoints({
1013
1032
  }),
1014
1033
  async onQueryStarted(arg, { dispatch, queryFulfilled }) {
1015
1034
  dispatch(setShippingStepBusy(true));
1016
- await queryFulfilled;
1017
- dispatch(setShippingStepBusy(false));
1035
+ try {
1036
+ await queryFulfilled;
1037
+ } finally {
1038
+ dispatch(setShippingStepBusy(false));
1039
+ }
1018
1040
  }
1019
1041
  }),
1020
1042
  setOrderSelectionPage: build.mutation<
package/data/urls.ts CHANGED
@@ -139,6 +139,8 @@ export const checkout = {
139
139
  couponSelectionPage: '/orders/checkout/?page=CouponSelectionPage',
140
140
  setAttributeBasedShippingOption:
141
141
  '/orders/checkout/?page=AttributeBasedShippingOptionSelectionPage',
142
+ setAttributeBasedShippingOptionPage: (pageName: string) =>
143
+ `/orders/checkout/?page=${pageName}`,
142
144
  deliveryBagsPage: '/orders/checkout/?page=DeliveryBagsPage',
143
145
  setOrderSelectionPage: '/orders/checkout/?page=OrderSelectionPage',
144
146
  loyaltyCardPage: '/orders/checkout/?page=LoyaltyCardPage',
package/jest.config.js CHANGED
@@ -12,7 +12,8 @@ module.exports = {
12
12
  roots: [path.resolve(__dirname)],
13
13
  transformIgnorePatterns: [],
14
14
  moduleNameMapper: {
15
- '^settings$': path.resolve(baseDir, 'src/settings.js')
15
+ '^settings$': path.resolve(baseDir, 'src/settings.js'),
16
+ '^@theme/(.*)$': path.resolve(baseDir, 'src/$1')
16
17
  },
17
18
  transform: {
18
19
  '^.+\\.(tsx?|jsx?|mjs?)$': [
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@akinon/next",
3
3
  "description": "Core package for Project Zero Next",
4
- "version": "1.126.14-v1-rc.10",
4
+ "version": "1.126.14",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -35,7 +35,7 @@
35
35
  "set-cookie-parser": "2.6.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@akinon/eslint-plugin-projectzero": "1.126.14-v1-rc.10",
38
+ "@akinon/eslint-plugin-projectzero": "1.126.14",
39
39
  "@babel/core": "7.26.10",
40
40
  "@babel/preset-env": "7.26.9",
41
41
  "@babel/preset-typescript": "7.27.0",
@@ -25,7 +25,10 @@ import {
25
25
  setUnavailablePaymentOptions
26
26
  } from '../../redux/reducers/checkout';
27
27
  import { RootState, TypedDispatch } from 'redux/store';
28
- import { checkoutApi } from '../../data/client/checkout';
28
+ import {
29
+ checkoutApi,
30
+ setAttributeBasedShippingPageName
31
+ } from '../../data/client/checkout';
29
32
  import { CheckoutContext, PreOrder } from '../../types';
30
33
  import { getCookie } from '../../utils';
31
34
  import settings from 'settings';
@@ -171,6 +174,7 @@ export const contextListMiddleware: Middleware = ({
171
174
  }
172
175
 
173
176
  if (context.page_context.attribute_based_shipping_options) {
177
+ setAttributeBasedShippingPageName(context.page_name);
174
178
  dispatch(
175
179
  setAttributeBasedShippingOptions(
176
180
  context.page_context.attribute_based_shipping_options
@@ -21,12 +21,28 @@ export const attributeBasedShippingOptionMiddleware: Middleware = ({
21
21
  Object.keys(attributeBasedShippingOptions).length > 0 &&
22
22
  !preOrder.attribute_based_shipping_options
23
23
  ) {
24
- const initialSelectedOptions: Record<string, number> = Object.fromEntries(
25
- Object.entries(attributeBasedShippingOptions).map(([key, options]) => [
26
- key,
27
- options.attribute_based_shipping_options[0].pk
28
- ])
29
- );
24
+ // A group can come back without any option: the service logs it and still
25
+ // reports the group with an empty list, so indexing into it would throw
26
+ // inside the middleware chain and drop the whole checkout response.
27
+ const initialSelectedOptions: Record<string, number> = {};
28
+
29
+ Object.entries(attributeBasedShippingOptions).forEach(([key, group]) => {
30
+ const pk = group?.attribute_based_shipping_options?.[0]?.pk;
31
+
32
+ if (typeof pk === 'number') {
33
+ initialSelectedOptions[key] = pk;
34
+ }
35
+ });
36
+
37
+ // The backend rejects a selection that does not cover every group it
38
+ // reported, and the rejection leaves pre_order empty, which brings us
39
+ // straight back here. Post nothing rather than start that loop.
40
+ if (
41
+ Object.keys(initialSelectedOptions).length !==
42
+ Object.keys(attributeBasedShippingOptions).length
43
+ ) {
44
+ return result;
45
+ }
30
46
 
31
47
  dispatch(
32
48
  apiEndpoints.setAttributeBasedShippingOptions.initiate(