@akinon/pz-theme 2.0.154-rc.6 → 2.0.154

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.
@@ -9,7 +9,6 @@ import { getResponsiveValue, partAttrs } from '../utils';
9
9
  import { formatDate, formatMoney } from './commerce-format';
10
10
  import { readMoneyStyle } from './money-style';
11
11
  import { hasItemTemplate, ItemTemplateList } from './item-template';
12
- import { renderOrderLineActions } from './order-line-actions';
13
12
  import { BlockRendererProps } from './block-renderer-registry';
14
13
  import {
15
14
  DesignerPlaceholder,
@@ -17,7 +16,7 @@ import {
17
16
  readToggle
18
17
  } from './product-block-utils';
19
18
 
20
- type OrderItemLike = {
19
+ interface OrderItemLike {
21
20
  id?: number;
22
21
  price?: string;
23
22
  quantity?: string | number;
@@ -27,6 +26,7 @@ type OrderItemLike = {
27
26
  tracking_number?: string;
28
27
  estimated_delivery_date?: string;
29
28
  price_currency?: { value?: string; label?: string };
29
+ is_refundable?: boolean;
30
30
  active_cancellation_request?: unknown;
31
31
  product?: {
32
32
  pk?: number;
@@ -35,7 +35,7 @@ type OrderItemLike = {
35
35
  absolute_url?: string;
36
36
  attributes?: Record<string, unknown>;
37
37
  } | null;
38
- };
38
+ }
39
39
 
40
40
  interface AddressLike {
41
41
  first_name?: string;
@@ -66,6 +66,7 @@ interface OrderLike {
66
66
  orderitem_set?: OrderItemLike[];
67
67
  discountitem_set?: DiscountItemLike[];
68
68
  is_cancellable?: boolean;
69
+ is_refundable?: boolean;
69
70
  e_archive_url?: string | null;
70
71
  shipping_address?: AddressLike | null;
71
72
  billing_address?: AddressLike | null;
@@ -113,6 +114,37 @@ const firstString = (
113
114
  return '';
114
115
  };
115
116
 
117
+ const hasOpenRequest = (request: unknown): boolean =>
118
+ Array.isArray(request) ? request.length > 0 : Boolean(request);
119
+
120
+ /** Commerce's "Teslim edildi". */
121
+ const DELIVERED = '550';
122
+
123
+ /**
124
+ * Whether the order still has a line to return: delivered, refundable, with
125
+ * no request open on it. Commerce also marks a shipped line refundable, and
126
+ * keeps `is_refundable` true on the order and the line while a return request
127
+ * is open, so the order flag alone would offer a return before the parcel
128
+ * arrives and again after the shopper has asked. Lines that carry no flag at
129
+ * all (an older payload) leave the decision to the order's flag and status.
130
+ */
131
+ const hasReturnableLine = (order: OrderLike): boolean => {
132
+ if (!order.is_refundable) return false;
133
+
134
+ const lines = order.orderitem_set ?? [];
135
+
136
+ if (!lines.some((item) => item && 'is_refundable' in item)) {
137
+ return String(order.status?.value ?? '') === DELIVERED;
138
+ }
139
+
140
+ return lines.some(
141
+ (item) =>
142
+ String(item?.status?.value ?? '') === DELIVERED &&
143
+ item?.is_refundable &&
144
+ !hasOpenRequest(item.active_cancellation_request)
145
+ );
146
+ };
147
+
116
148
  /** Positive integers only; anything else falls back to the shipped size. */
117
149
  const readSize = (value: unknown, fallback: number): number => {
118
150
  const numeric = Math.round(Number(value));
@@ -146,6 +178,7 @@ const DESIGNER_SAMPLE_ORDER: OrderLike = {
146
178
  status: { value: '10', label: 'Approved' },
147
179
  currency: { value: 'try', label: 'TRY' },
148
180
  is_cancellable: true,
181
+ is_refundable: true,
149
182
  e_archive_url: '#',
150
183
  installment_count: 3,
151
184
  orderitem_set: [
@@ -159,7 +192,16 @@ const DESIGNER_SAMPLE_ORDER: OrderLike = {
159
192
  tracking_number: '1Z999AA10123456784',
160
193
  tracking_url: '#',
161
194
  active_cancellation_request: [
162
- { easy_return: { code: '12345678912345' } }
195
+ {
196
+ easy_return: {
197
+ code: '12345678912345',
198
+ shipping_company: { value: 'carrier', label: 'Carrier' }
199
+ },
200
+ pickup_date: {
201
+ date: '2026-04-11',
202
+ hour_range: { start: '09:00:00', end: '18:00:00' }
203
+ }
204
+ }
163
205
  ],
164
206
  product: {
165
207
  pk: 1,
@@ -250,6 +292,63 @@ const resolveReturnCode = (value: unknown): string => {
250
292
  return typeof code === 'string' ? code : '';
251
293
  };
252
294
 
295
+ interface ReturnRequestLike {
296
+ pickup_date?: { date?: string } | null;
297
+ easy_return?: {
298
+ code?: string;
299
+ shipping_company?: { label?: string } | null;
300
+ } | null;
301
+ }
302
+
303
+ /**
304
+ * How a returned line goes back: `appointment` when the request booked a
305
+ * pickup (it carries a pickup day), `standard` for an easy return dropped off
306
+ * at a branch, nothing for a plain cancellation. The carrier and the pickup
307
+ * day come from the same request; both shapes of the field are probed, as
308
+ * for the return code.
309
+ */
310
+ const resolveReturnDetails = (
311
+ value: unknown
312
+ ): {
313
+ method: 'appointment' | 'standard' | null;
314
+ carrier: string;
315
+ pickupDate: string;
316
+ } => {
317
+ const entry = (Array.isArray(value) ? value[0] : value) as
318
+ | ReturnRequestLike
319
+ | null
320
+ | undefined;
321
+ const pickupDate = entry?.pickup_date?.date;
322
+ const carrier = entry?.easy_return?.shipping_company?.label;
323
+
324
+ return {
325
+ method: pickupDate ? 'appointment' : entry?.easy_return ? 'standard' : null,
326
+ carrier: typeof carrier === 'string' ? carrier : '',
327
+ pickupDate: typeof pickupDate === 'string' ? pickupDate : ''
328
+ };
329
+ };
330
+
331
+ /** "2026-04-11" read as a calendar day in the page's locale (no time zone shift). */
332
+ const formatPickupDate = (value: string, locale?: string): string => {
333
+ const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(value);
334
+ if (!match) return value;
335
+
336
+ try {
337
+ return new Intl.DateTimeFormat(locale || undefined, {
338
+ timeZone: 'UTC',
339
+ day: '2-digit',
340
+ month: '2-digit',
341
+ year: 'numeric'
342
+ }).format(
343
+ new Date(
344
+ Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]), 12)
345
+ )
346
+ );
347
+ } catch {
348
+ return value;
349
+ }
350
+ };
351
+
253
352
  const formatAddress = (address: AddressLike | null | undefined): string[] => {
254
353
  if (!address) return [];
255
354
  const name = [address.first_name, address.last_name]
@@ -273,16 +372,16 @@ const formatAddress = (address: AddressLike | null | undefined): string[] => {
273
372
  * order key is read from the URL; the editor owns which sections are exposed,
274
373
  * every caption and the styling.
275
374
  * A designer can mark one child as the row layout for the order lines; without
276
- * one the built-in line renders. A brand may hang per-line actions on the
277
- * built-in line through `registerOrderLineActions`; they render inside the
278
- * line's `item-actions` part.
375
+ * one the built-in line renders.
279
376
  * Properties: numberLabel, numberTag, dateLabel, headerTotalLabel,
280
377
  * showInvoiceLink, invoiceLinkLabel, showCancelLink, cancelLinkLabel,
281
- * cancelHrefBase, showShipmentStatus, showItems, showImage, imageWidth,
378
+ * showReturnLink, returnLinkLabel, cancelHrefBase, showShipmentStatus, showItems, showImage, imageWidth,
282
379
  * imageHeight, showQuantity, quantityLabel, showColor,
283
380
  * colorLabel, colorAttributeKey, showSize, sizeLabel, sizeAttributeKey,
284
381
  * showLineTracking, lineTrackingLabel, showReturnCode,
285
- * returnCodeLabel, showShippingAddress, shippingAddressLabel,
382
+ * returnCodeLabel, showReturnMethod, returnMethodLabel,
383
+ * appointmentReturnLabel, standardReturnLabel, showReturnCarrier,
384
+ * returnCarrierLabel, showPickupDate, pickupDateLabel, showShippingAddress, shippingAddressLabel,
286
385
  * showBillingAddress, billingAddressLabel, showTotals, showPieceCount,
287
386
  * pieceCountLabel, showDiscountItems, showInstallmentInfo,
288
387
  * installmentInfoLabel, subtotalLabel, discountLabel, shippingLabel,
@@ -290,8 +389,9 @@ const formatAddress = (address: AddressLike | null | undefined): string[] => {
290
389
  * showPaymentInfo, paymentInfoLabel, decimals, currencyDisplay,
291
390
  * currencyText, currencyPosition.
292
391
  * Parts: detail, header, number, date, header-total, status,
293
- * invoice-link, cancel-link, list, item, image, name, quantity, color, size,
294
- * item-status, item-tracking, item-return-code, item-actions, price, shipment,
392
+ * invoice-link, cancel-link, return-link, list, item, image, name, quantity,
393
+ * color, size, item-status, item-tracking, item-return-code,
394
+ * item-return-method, item-return-carrier, item-pickup-date, price, shipment,
295
395
  * delivery, tracking, addresses, address, address-label, totals, total-row,
296
396
  * total-label, total-value, grand-total, payment-info, payment-info-label.
297
397
  */
@@ -402,6 +502,38 @@ const OrderDetailBlock = ({
402
502
  false
403
503
  );
404
504
  const returnCodeLabel = text('returnCodeLabel', 'Return code:');
505
+ // ZERO-5183: the easy-return screens start from here, and a returned line
506
+ // says how it goes back. The cancel link only shows while the order can
507
+ // still be cancelled; a delivered order needs its own way in.
508
+ const showReturnLink = readToggle(
509
+ block.properties?.showReturnLink,
510
+ currentBreakpoint,
511
+ false
512
+ );
513
+ const returnLinkLabel = text('returnLinkLabel', 'Easy return');
514
+ const showReturnMethod = readToggle(
515
+ block.properties?.showReturnMethod,
516
+ currentBreakpoint,
517
+ false
518
+ );
519
+ const returnMethodLabel = text('returnMethodLabel', 'Return method:');
520
+ const appointmentReturnLabel = text(
521
+ 'appointmentReturnLabel',
522
+ 'Scheduled pickup'
523
+ );
524
+ const standardReturnLabel = text('standardReturnLabel', 'Standard');
525
+ const showReturnCarrier = readToggle(
526
+ block.properties?.showReturnCarrier,
527
+ currentBreakpoint,
528
+ false
529
+ );
530
+ const returnCarrierLabel = text('returnCarrierLabel', 'Carrier:');
531
+ const showPickupDate = readToggle(
532
+ block.properties?.showPickupDate,
533
+ currentBreakpoint,
534
+ false
535
+ );
536
+ const pickupDateLabel = text('pickupDateLabel', 'Pickup date:');
405
537
 
406
538
  const showShippingAddress = readToggle(
407
539
  block.properties?.showShippingAddress,
@@ -489,6 +621,12 @@ const OrderDetailBlock = ({
489
621
  const money = (value: unknown) =>
490
622
  formatMoney(value, { locale, currency, ...moneyStyle });
491
623
 
624
+ // The sample order is not a delivered one; it shows the link for styling.
625
+ const offersReturn =
626
+ order === DESIGNER_SAMPLE_ORDER
627
+ ? Boolean(order.is_refundable)
628
+ : hasReturnableLine(order);
629
+
492
630
  // Navigating inside the designer iframe would drop the author out of the page
493
631
  // they are editing, so every link is inert there.
494
632
  const guard = (event: React.MouseEvent) => {
@@ -667,7 +805,7 @@ const OrderDetailBlock = ({
667
805
  {headerTotalLabel} {money(order.amount)}
668
806
  </span>
669
807
  )}
670
- {(showInvoiceLink || showCancelLink) && (
808
+ {(showInvoiceLink || showCancelLink || showReturnLink) && (
671
809
  <div
672
810
  style={{
673
811
  display: 'flex',
@@ -707,6 +845,21 @@ const OrderDetailBlock = ({
707
845
  → {cancelLinkLabel}
708
846
  </NextLink>
709
847
  )}
848
+ {showReturnLink && offersReturn && cancelHref && (
849
+ <NextLink
850
+ href={isDesigner ? '#' : cancelHref}
851
+ onClick={guard}
852
+ data-order-detail-return-link=""
853
+ {...partAttrs('return-link')}
854
+ style={{
855
+ color: 'inherit',
856
+ textDecoration: 'none',
857
+ whiteSpace: 'nowrap'
858
+ }}
859
+ >
860
+ → {returnLinkLabel}
861
+ </NextLink>
862
+ )}
710
863
  </div>
711
864
  )}
712
865
  </div>
@@ -761,14 +914,22 @@ const OrderDetailBlock = ({
761
914
  const returnCodeValue = showReturnCode
762
915
  ? resolveReturnCode(item.active_cancellation_request)
763
916
  : '';
764
- const lineActions = renderOrderLineActions({
765
- order,
766
- line,
767
- isDesigner
768
- });
769
- const showLineMeta =
770
- showShipmentStatus &&
771
- Boolean(item.status?.label || trackingValue || returnCodeValue);
917
+ const returnDetails = resolveReturnDetails(
918
+ item.active_cancellation_request
919
+ );
920
+ const returnMethodValue =
921
+ showReturnMethod && returnDetails.method
922
+ ? returnDetails.method === 'appointment'
923
+ ? appointmentReturnLabel
924
+ : standardReturnLabel
925
+ : '';
926
+ const returnCarrierValue = showReturnCarrier
927
+ ? returnDetails.carrier
928
+ : '';
929
+ const pickupDateValue =
930
+ showPickupDate && returnDetails.pickupDate
931
+ ? formatPickupDate(returnDetails.pickupDate, locale)
932
+ : '';
772
933
 
773
934
  return (
774
935
  <li
@@ -884,68 +1045,98 @@ const OrderDetailBlock = ({
884
1045
  </div>
885
1046
  </div>
886
1047
 
887
- {(showLineMeta || lineActions) && (
888
- <div
889
- style={{
890
- display: 'flex',
891
- flexDirection: 'column',
892
- alignItems: isMobile ? 'flex-start' : 'flex-end',
893
- textAlign: isMobile ? 'left' : 'right',
894
- gap: '4px',
895
- flex: isMobile ? '1 1 100%' : '0 0 auto'
896
- }}
897
- >
898
- {lineActions && (
899
- <div
900
- data-order-detail-item-actions=""
901
- {...partAttrs('item-actions')}
902
- >
903
- {lineActions}
904
- </div>
905
- )}
906
- {showLineMeta && item.status?.label && (
907
- <span
908
- data-order-detail-item-status=""
909
- data-order-status-value={item.status.value}
910
- {...partAttrs('item-status')}
911
- style={{ display: 'inline-block' }}
912
- >
913
- {item.status.label}
914
- </span>
915
- )}
916
- {showLineMeta && trackingValue && (
917
- <span
918
- data-order-detail-item-tracking=""
919
- {...partAttrs('item-tracking')}
920
- style={{ opacity: 0.8 }}
921
- >
922
- {lineTrackingLabel}{' '}
923
- {item.tracking_url ? (
924
- <a
925
- href={isDesigner ? '#' : item.tracking_url}
926
- onClick={guard}
927
- target={isDesigner ? undefined : '_blank'}
928
- rel="noreferrer"
929
- style={{ color: 'inherit' }}
930
- >
931
- {item.tracking_number || item.tracking_url}
932
- </a>
933
- ) : (
934
- trackingValue
935
- )}
936
- </span>
937
- )}
938
- {showLineMeta && returnCodeValue && (
939
- <span
940
- data-order-detail-item-return-code=""
941
- {...partAttrs('item-return-code')}
942
- style={{ opacity: 0.8 }}
943
- >
944
- {returnCodeLabel} {returnCodeValue}
945
- </span>
946
- )}
947
- </div>
948
- )}
1048
+ {showShipmentStatus &&
1049
+ (item.status?.label ||
1050
+ trackingValue ||
1051
+ returnCodeValue ||
1052
+ returnMethodValue ||
1053
+ returnCarrierValue ||
1054
+ pickupDateValue) && (
1055
+ <div
1056
+ style={{
1057
+ display: 'flex',
1058
+ flexDirection: 'column',
1059
+ alignItems: isMobile ? 'flex-start' : 'flex-end',
1060
+ textAlign: isMobile ? 'left' : 'right',
1061
+ gap: '4px',
1062
+ flex: isMobile ? '1 1 100%' : '0 0 auto'
1063
+ }}
1064
+ >
1065
+ {item.status?.label && (
1066
+ <span
1067
+ data-order-detail-item-status=""
1068
+ data-order-status-value={item.status.value}
1069
+ {...partAttrs('item-status')}
1070
+ style={{ display: 'inline-block' }}
1071
+ >
1072
+ {item.status.label}
1073
+ </span>
1074
+ )}
1075
+ {/* Label and value in their own spans, so a
1076
+ composition can colour them apart. */}
1077
+ {returnMethodValue && (
1078
+ <span
1079
+ data-order-detail-item-return-method=""
1080
+ {...partAttrs('item-return-method')}
1081
+ style={{ opacity: 0.8 }}
1082
+ >
1083
+ <span data-label="">{returnMethodLabel}</span>{' '}
1084
+ <span data-value="">{returnMethodValue}</span>
1085
+ </span>
1086
+ )}
1087
+ {returnCarrierValue && (
1088
+ <span
1089
+ data-order-detail-item-return-carrier=""
1090
+ {...partAttrs('item-return-carrier')}
1091
+ style={{ opacity: 0.8 }}
1092
+ >
1093
+ <span data-label="">{returnCarrierLabel}</span>{' '}
1094
+ <span data-value="">{returnCarrierValue}</span>
1095
+ </span>
1096
+ )}
1097
+ {pickupDateValue && (
1098
+ <span
1099
+ data-order-detail-item-pickup-date=""
1100
+ {...partAttrs('item-pickup-date')}
1101
+ style={{ opacity: 0.8 }}
1102
+ >
1103
+ <span data-label="">{pickupDateLabel}</span>{' '}
1104
+ <span data-value="">{pickupDateValue}</span>
1105
+ </span>
1106
+ )}
1107
+ {trackingValue && (
1108
+ <span
1109
+ data-order-detail-item-tracking=""
1110
+ {...partAttrs('item-tracking')}
1111
+ style={{ opacity: 0.8 }}
1112
+ >
1113
+ {lineTrackingLabel}{' '}
1114
+ {item.tracking_url ? (
1115
+ <a
1116
+ href={isDesigner ? '#' : item.tracking_url}
1117
+ onClick={guard}
1118
+ target={isDesigner ? undefined : '_blank'}
1119
+ rel="noreferrer"
1120
+ style={{ color: 'inherit' }}
1121
+ >
1122
+ {item.tracking_number || item.tracking_url}
1123
+ </a>
1124
+ ) : (
1125
+ trackingValue
1126
+ )}
1127
+ </span>
1128
+ )}
1129
+ {returnCodeValue && (
1130
+ <span
1131
+ data-order-detail-item-return-code=""
1132
+ {...partAttrs('item-return-code')}
1133
+ style={{ opacity: 0.8 }}
1134
+ >
1135
+ {returnCodeLabel} {returnCodeValue}
1136
+ </span>
1137
+ )}
1138
+ </div>
1139
+ )}
949
1140
  </div>
950
1141
  </li>
951
1142
  );
@@ -13,14 +13,7 @@ import { readToggle } from './product-block-utils';
13
13
  import { BlockRendererProps } from './block-renderer-registry';
14
14
 
15
15
  interface OrderItemLike {
16
- id?: number;
17
- status?: { value?: string | number; label?: string };
18
- product?: {
19
- image?: string;
20
- name?: string;
21
- sku?: string;
22
- base_code?: string;
23
- } | null;
16
+ product?: { image?: string; name?: string } | null;
24
17
  }
25
18
 
26
19
  interface OrderLike {
@@ -255,23 +248,7 @@ const OrderListBlock = ({
255
248
  maxImages
256
249
  ),
257
250
  url: hrefFor(order),
258
- href: hrefFor(order),
259
- createdDate: order.created_date || '',
260
- items: (Array.isArray(order.orderitem_set) ? order.orderitem_set : []).map(
261
- (item, itemIndex) => ({
262
- id: item?.id ?? itemIndex,
263
- name: item?.product?.name || '',
264
- image: item?.product?.image || '',
265
- status: item?.status?.label || '',
266
- statusValue: String(item?.status?.value ?? ''),
267
- product: {
268
- sku: item?.product?.sku || '',
269
- base_code: item?.product?.base_code || '',
270
- name: item?.product?.name || '',
271
- image: item?.product?.image || ''
272
- }
273
- })
274
- )
251
+ href: hrefFor(order)
275
252
  }));
276
253
 
277
254
  // A designer who marked a child as the row layout gets that layout repeated
@@ -240,6 +240,8 @@ export interface OrderLineLike {
240
240
  price?: string | number;
241
241
  quantity?: string | number;
242
242
  total_amount?: string | number;
243
+ status?: { value?: string | number; label?: string } | null;
244
+ active_cancellation_request?: unknown;
243
245
  product?: {
244
246
  pk?: number;
245
247
  name?: string;
@@ -278,7 +280,10 @@ export const getLineImage = (
278
280
  };
279
281
 
280
282
  export interface GroupedOrderLine<T extends OrderLineLike> {
281
- /** Stable React key: the product pk when the payload carries one. */
283
+ /**
284
+ * Stable React key: the product pk when the payload carries one (with a
285
+ * `-2`, `-3`… suffix for the product's further rows, see groupOrderItems).
286
+ */
282
287
  key: string;
283
288
  name: string;
284
289
  image: string;
@@ -301,21 +306,28 @@ export interface GroupedOrderLine<T extends OrderLineLike> {
301
306
  * `quantity` is summed rather than counted so a payload that does carry a line
302
307
  * quantity (older commerce versions, and brands that merge lines server-side)
303
308
  * is respected instead of being flattened back to one.
309
+ *
310
+ * Units of one product only share a row while nothing sets them apart: once
311
+ * one of them is cancelled or returned (its status moves on, or it carries a
312
+ * cancellation / return request) it gets a row of its own. A shared row shows
313
+ * its first unit's status and return details, so "return 1 of 2" would
314
+ * otherwise read as both units returned.
304
315
  */
305
316
  export const groupOrderItems = <T extends OrderLineLike>(
306
317
  items: T[] | null | undefined
307
318
  ): GroupedOrderLine<T>[] => {
308
319
  const source = Array.isArray(items) ? items : [];
320
+ const groups: GroupedOrderLine<T>[] = [];
321
+ const byIdentity = new Map<string, GroupedOrderLine<T>>();
322
+ const rowsPerKey = new Map<string, number>();
309
323
 
310
- return source.reduce<GroupedOrderLine<T>[]>((accumulator, item) => {
324
+ source.forEach((item) => {
311
325
  const pk = item.product?.pk;
312
- const key = String(pk ?? item.id ?? accumulator.length);
313
326
  // Lines without a product pk cannot be proven identical, so they stay
314
327
  // separate rows instead of collapsing into an arbitrary neighbour.
315
- const existing =
316
- pk === undefined || pk === null
317
- ? undefined
318
- : accumulator.find((entry) => entry.key === key);
328
+ const identity =
329
+ pk === undefined || pk === null ? null : `${pk}#${lineState(item)}`;
330
+ const existing = identity === null ? undefined : byIdentity.get(identity);
319
331
  const quantity = parseAmount(item.quantity) ?? 1;
320
332
  const lineTotal = parseAmount(item.total_amount || item.price);
321
333
 
@@ -324,11 +336,15 @@ export const groupOrderItems = <T extends OrderLineLike>(
324
336
  existing.total =
325
337
  lineTotal === null ? existing.total : (existing.total ?? 0) + lineTotal;
326
338
 
327
- return accumulator;
339
+ return;
328
340
  }
329
341
 
330
- accumulator.push({
331
- key,
342
+ const baseKey = String(pk ?? item.id ?? groups.length);
343
+ const rows = (rowsPerKey.get(baseKey) ?? 0) + 1;
344
+ rowsPerKey.set(baseKey, rows);
345
+
346
+ const group: GroupedOrderLine<T> = {
347
+ key: rows === 1 ? baseKey : `${baseKey}-${rows}`,
332
348
  name: item.product?.name || '',
333
349
  image: getLineImage(item),
334
350
  href: item.product?.absolute_url || '',
@@ -336,10 +352,30 @@ export const groupOrderItems = <T extends OrderLineLike>(
336
352
  total: lineTotal,
337
353
  quantity,
338
354
  item
339
- });
355
+ };
356
+ groups.push(group);
357
+ if (identity !== null) byIdentity.set(identity, group);
358
+ });
359
+
360
+ return groups;
361
+ };
362
+
363
+ /**
364
+ * What tells one unit of a product apart from another in the same order: its
365
+ * status and the cancellation / return request it carries (one object or a
366
+ * one-item array, depending on the pz version).
367
+ */
368
+ const lineState = (item: OrderLineLike): string => {
369
+ const value = item.active_cancellation_request;
370
+ const request = (Array.isArray(value) ? value[0] : value) as
371
+ | { id?: unknown; easy_return?: { code?: unknown } | null }
372
+ | null
373
+ | undefined;
374
+ const requestKey = request
375
+ ? String(request.id ?? request.easy_return?.code ?? 'request')
376
+ : '';
340
377
 
341
- return accumulator;
342
- }, []);
378
+ return `${item.status?.value ?? ''}|${requestKey}`;
343
379
  };
344
380
 
345
381
  /**
@@ -41,7 +41,7 @@ const orderPayload = {
41
41
  orderitem_set: [
42
42
  {
43
43
  price: '649.95',
44
- status: { value: '500', label: 'Shipped' },
44
+ status: { label: 'Shipped' },
45
45
  product: {
46
46
  name: 'Spring Sneaker',
47
47
  image: '/i.jpg',
@@ -63,19 +63,6 @@ describe('buildOrderPageContext', () => {
63
63
  expect(context.order.currency).toBe('TRY');
64
64
  expect(context.order.items).toHaveLength(1);
65
65
  expect(context.order.items[0].title).toBe('Spring Sneaker');
66
- expect(context.order.items[0].status).toBe('Shipped');
67
- expect(context.order.items[0].statusValue).toBe('500');
68
- });
69
-
70
- it('stringifies a numeric order item status code', () => {
71
- const context = buildOrderPageContext({
72
- order: {
73
- ...orderPayload,
74
- orderitem_set: [{ status: { value: 550, label: 'Delivered' } }]
75
- }
76
- });
77
-
78
- expect(context.order.items[0].statusValue).toBe('550');
79
66
  });
80
67
 
81
68
  it('does not publish the identity fields the raw payload carries', () => {
@@ -59,9 +59,6 @@ const toRecord = (value: unknown): UnknownRecord =>
59
59
  const toStringValue = (value: unknown, fallback = ''): string =>
60
60
  typeof value === 'string' ? value : fallback;
61
61
 
62
- const toCodeValue = (value: unknown): string =>
63
- typeof value === 'number' ? String(value) : toStringValue(value);
64
-
65
62
  const humanizeSlug = (value: string) =>
66
63
  value
67
64
  .split('-')
@@ -482,7 +479,6 @@ export const buildOrderPageContext = ({
482
479
  unitPrice: toStringValue(item.price),
483
480
  quantity: 1,
484
481
  status: toStringValue(toRecord(item.status).label),
485
- statusValue: toCodeValue(toRecord(item.status).value),
486
482
  currency
487
483
  };
488
484
  });