@faststore/api 4.9.2 → 4.9.3-dev.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.
@@ -1,32 +1,125 @@
1
1
  import type { Channel } from './channel'
2
2
  import ChannelMarshal from './channel'
3
3
 
4
+ /**
5
+ * Why the session SC must not be replaced by Session Manager:
6
+ * - `orderForm`: adopted from an orderForm with items (e.g. after Quick Order);
7
+ * kept until the cart is empty.
8
+ * - `url`: derived from the URL by localization; always sent to Checkout.
9
+ *
10
+ * Lives only in the raw `channel` JSON string so the public `Channel` type
11
+ * stays unchanged.
12
+ */
13
+ export type SalesChannelSource = 'orderForm' | 'url'
14
+
15
+ const parseRawChannel = (channel: string | null | undefined) => {
16
+ try {
17
+ return JSON.parse(channel ?? '') ?? {}
18
+ } catch {
19
+ return {}
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Reads the marker from the raw channel string. A `url` marker only means
25
+ * something while localization is enabled; a leftover one (e.g. after a store
26
+ * turns localization off) is ignored so Session Manager decides again.
27
+ */
28
+ export function salesChannelSourceOf(
29
+ channel: string | null | undefined,
30
+ { localizationEnabled = true }: { localizationEnabled?: boolean } = {}
31
+ ): SalesChannelSource | undefined {
32
+ const source = parseRawChannel(channel).salesChannelSource
33
+
34
+ if (source === 'url') {
35
+ return localizationEnabled ? source : undefined
36
+ }
37
+
38
+ return source === 'orderForm' ? source : undefined
39
+ }
40
+
41
+ /** Whether the store config enables localization (URL-derived SC). */
42
+ export const isLocalizationEnabled = (
43
+ discoveryConfig: Record<string, unknown> | undefined
44
+ ) =>
45
+ Boolean(
46
+ (discoveryConfig?.localization as { enabled?: boolean } | undefined)
47
+ ?.enabled
48
+ )
49
+
50
+ /**
51
+ * SC that Session Manager rejected (401/403) for this shopper after it was
52
+ * adopted from the orderForm. validateCart must not adopt it again, otherwise
53
+ * adoption and rejection would loop.
54
+ */
55
+ export function rejectedSalesChannelOf(
56
+ channel: string | null | undefined
57
+ ): string | undefined {
58
+ const rejected = parseRawChannel(channel).rejectedSalesChannel
59
+
60
+ return typeof rejected === 'string' || typeof rejected === 'number'
61
+ ? String(rejected) || undefined
62
+ : undefined
63
+ }
64
+
4
65
  /**
5
66
  * Builds the session `channel` string after Session Manager responds.
6
67
  *
7
- * When the client already pinned an explicit SC (e.g. adopted from the
8
- * orderForm after Quick Order), keep it. Session Manager often still reports
9
- * the default SC and would otherwise overwrite the adoption.
68
+ * Session Manager is the SC authority, except when the client channel carries
69
+ * a `salesChannelSource` marker: then the client SC is kept and the marker is
70
+ * re-emitted so it survives this validation. A recorded rejection is also
71
+ * re-emitted until Session Manager resolves that SC for the shopper.
10
72
  */
11
73
  export function channelAfterSessionManager(
12
74
  channel: Required<Channel>,
13
75
  storeChannelValue: string | null | undefined,
14
76
  regionId: string | null | undefined,
15
- sellerId: string | undefined
77
+ sellerId: string | undefined,
78
+ salesChannelSource?: SalesChannelSource,
79
+ rejectedSalesChannel?: string
16
80
  ): string {
17
- const hasExplicitSalesChannel = channel.hasOnlyDefaultSalesChannel === false
18
-
19
81
  const storeSalesChannel = storeChannelValue ?? undefined
20
82
  const resolvedRegionId = regionId ?? channel.regionId
83
+ const salesChannel = salesChannelSource
84
+ ? channel.salesChannel || storeSalesChannel
85
+ : (storeSalesChannel ?? channel.salesChannel)
86
+ const keepRejection =
87
+ rejectedSalesChannel != null &&
88
+ rejectedSalesChannel !== String(salesChannel ?? '')
21
89
 
22
90
  return ChannelMarshal.stringify({
23
- salesChannel: hasExplicitSalesChannel
24
- ? channel.salesChannel || storeSalesChannel
25
- : (storeSalesChannel ?? channel.salesChannel),
91
+ salesChannel,
26
92
  regionId: resolvedRegionId ?? undefined,
27
93
  seller: sellerId,
28
- hasOnlyDefaultSalesChannel: hasExplicitSalesChannel
29
- ? false
30
- : !storeSalesChannel,
94
+ hasOnlyDefaultSalesChannel: !storeSalesChannel,
95
+ ...(salesChannelSource ? { salesChannelSource } : {}),
96
+ ...(keepRejection ? { rejectedSalesChannel } : {}),
31
97
  })
32
98
  }
99
+
100
+ /** Checkout answers with this SC when an orderForm request carries no `sc`. */
101
+ const PLATFORM_DEFAULT_SALES_CHANNEL = '1'
102
+
103
+ /**
104
+ * SC to request from Session Manager. FastStore 4.6–4.9 could move a session
105
+ * to the SC 1 fallback of an empty orderForm and pin it there. When an
106
+ * unmarked session is on SC 1 but the store runs on another SC, ask for the
107
+ * store SC instead: if Session Manager assigns SC 1 on purpose (e.g. by
108
+ * profile), it still answers SC 1.
109
+ */
110
+ export function salesChannelToRequest(
111
+ channel: Required<Channel>,
112
+ salesChannelSource: SalesChannelSource | undefined,
113
+ storeSalesChannel: string | undefined,
114
+ search: string
115
+ ): string {
116
+ const salesChannel = String(channel.salesChannel ?? '')
117
+ const isStuckOnFallback =
118
+ salesChannelSource === undefined &&
119
+ salesChannel === PLATFORM_DEFAULT_SALES_CHANNEL &&
120
+ !!storeSalesChannel &&
121
+ storeSalesChannel !== PLATFORM_DEFAULT_SALES_CHANNEL &&
122
+ !new URLSearchParams(search).has('sc')
123
+
124
+ return isStuckOnFallback ? storeSalesChannel : salesChannel
125
+ }