@faststore/api 4.9.3-dev.0 → 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.
- package/dist/cjs/index.js +46 -46
- package/dist/cjs/index.js.map +1 -1
- package/dist/es/index.mjs +1169 -1075
- package/dist/es/index.mjs.map +1 -1
- package/dist/src/observability/telemetry.d.ts +2 -0
- package/dist/src/observability/telemetry.d.ts.map +1 -1
- package/dist/src/platforms/vtex/index.d.ts +1 -1
- package/dist/src/platforms/vtex/resolvers/index.d.ts +1 -1
- package/dist/src/platforms/vtex/resolvers/mutation.d.ts +1 -1
- package/dist/src/platforms/vtex/resolvers/validateCart.d.ts.map +1 -1
- package/dist/src/platforms/vtex/resolvers/validateSession.d.ts +1 -1
- package/dist/src/platforms/vtex/resolvers/validateSession.d.ts.map +1 -1
- package/dist/src/platforms/vtex/utils/cartSalesChannel.d.ts +8 -0
- package/dist/src/platforms/vtex/utils/cartSalesChannel.d.ts.map +1 -1
- package/dist/src/platforms/vtex/utils/sessionChannel.d.ts +39 -4
- package/dist/src/platforms/vtex/utils/sessionChannel.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/observability/telemetry.ts +2 -1
- package/src/platforms/vtex/resolvers/validateCart.ts +75 -11
- package/src/platforms/vtex/resolvers/validateSession.ts +147 -9
- package/src/platforms/vtex/utils/cartSalesChannel.ts +33 -0
- package/src/platforms/vtex/utils/sessionChannel.ts +105 -12
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** OpenTelemetry log emitter; a no-op while telemetry is disabled. */
|
|
2
|
+
export declare const OTELLogger: (severity: import('@faststore/diagnostics').LogSeverity, ...args: unknown[]) => void;
|
|
1
3
|
export declare const ResolverTrace: <TContext extends {
|
|
2
4
|
OTEL_ENABLED: boolean;
|
|
3
5
|
account: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../../../src/observability/telemetry.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../../../src/observability/telemetry.ts"],"names":[],"mappings":"AAGA,sEAAsE;AACtE,eAAO,MAAM,UAAU,sFAA2B,CAAA;AAElD,eAAO,MAAM,aAAa,GACxB,QAAQ,SAAS;IACf,YAAY,EAAE,OAAO,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACrC,EACD,OAAO,GAAG,GAAG,EACb,KAAK,GAAG,GAAG,EACX,OAAO,GAAG,GAAG,EAEb,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,KAAK,OAAO,EAC3E,qBAAyC,MAGvC,QAAQ,OAAO,EACf,MAAM,KAAK,EACX,gBAAgB,QAAQ,EACxB,MAAM,GAAG,KACR,OA2CJ,CAAA"}
|
|
@@ -330,7 +330,7 @@ export declare function getResolvers(): {
|
|
|
330
330
|
status: any;
|
|
331
331
|
}[];
|
|
332
332
|
} | null>;
|
|
333
|
-
validateSession: (_: any, { session: oldSession, search }: import('../..').MutationValidateSessionArgs, { clients, headers, account }: GraphqlContext) => Promise<import('../..').StoreSession | null>;
|
|
333
|
+
validateSession: (_: any, { session: oldSession, search }: import('../..').MutationValidateSessionArgs, { clients, headers, account, storage, discoveryConfig }: GraphqlContext) => Promise<import('../..').StoreSession | null>;
|
|
334
334
|
subscribeToNewsletter: (_: any, { data }: import('../..').MutationSubscribeToNewsletterArgs, { clients: { commerce } }: GraphqlContext) => Promise<import('../..').PersonNewsletter | null>;
|
|
335
335
|
cancelOrder: (_: any, { data }: import('../..').MutationCancelOrderArgs, { clients: { commerce } }: GraphqlContext) => Promise<import('../..').UserOrderCancel | null>;
|
|
336
336
|
processOrderAuthorization: (_: any, { data }: {
|
|
@@ -289,7 +289,7 @@ export declare const getResolvers: () => {
|
|
|
289
289
|
status: any;
|
|
290
290
|
}[];
|
|
291
291
|
} | null>;
|
|
292
|
-
validateSession: (_: any, { session: oldSession, search }: import('../../..').MutationValidateSessionArgs, { clients, headers, account }: import('..').GraphqlContext) => Promise<import('../../..').StoreSession | null>;
|
|
292
|
+
validateSession: (_: any, { session: oldSession, search }: import('../../..').MutationValidateSessionArgs, { clients, headers, account, storage, discoveryConfig }: import('..').GraphqlContext) => Promise<import('../../..').StoreSession | null>;
|
|
293
293
|
subscribeToNewsletter: (_: any, { data }: import('../../..').MutationSubscribeToNewsletterArgs, { clients: { commerce } }: import('..').GraphqlContext) => Promise<import('../../..').PersonNewsletter | null>;
|
|
294
294
|
cancelOrder: (_: any, { data }: import('../../..').MutationCancelOrderArgs, { clients: { commerce } }: import('..').GraphqlContext) => Promise<import('../../..').UserOrderCancel | null>;
|
|
295
295
|
processOrderAuthorization: (_: any, { data }: {
|
|
@@ -57,7 +57,7 @@ export declare const Mutation: {
|
|
|
57
57
|
status: any;
|
|
58
58
|
}[];
|
|
59
59
|
} | null>;
|
|
60
|
-
validateSession: (_: any, { session: oldSession, search }: import('../../..').MutationValidateSessionArgs, { clients, headers, account }: import('..').GraphqlContext) => Promise<import('../../..').StoreSession | null>;
|
|
60
|
+
validateSession: (_: any, { session: oldSession, search }: import('../../..').MutationValidateSessionArgs, { clients, headers, account, storage, discoveryConfig }: import('..').GraphqlContext) => Promise<import('../../..').StoreSession | null>;
|
|
61
61
|
subscribeToNewsletter: (_: any, { data }: import('../../..').MutationSubscribeToNewsletterArgs, { clients: { commerce } }: import('..').GraphqlContext) => Promise<import('../../..').PersonNewsletter | null>;
|
|
62
62
|
cancelOrder: (_: any, { data }: import('../../..').MutationCancelOrderArgs, { clients: { commerce } }: import('..').GraphqlContext) => Promise<import('../../..').UserOrderCancel | null>;
|
|
63
63
|
processOrderAuthorization: (_: any, { data }: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validateCart.d.ts","sourceRoot":"","sources":["../../../../../src/platforms/vtex/resolvers/validateCart.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validateCart.d.ts","sourceRoot":"","sources":["../../../../../src/platforms/vtex/resolvers/validateCart.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,IAAI,CAAA;AACxC,OAAO,KAAK,EAMV,wBAAwB,EACzB,MAAM,+BAA+B,CAAA;AAybtC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,YAAY,GACvB,GAAG,OAAO,EACV,8BAA8B,wBAAwB,EACtD,KAAK,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;SAqPpB,CAAA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { GraphqlContext } from '..';
|
|
2
2
|
import { MutationValidateSessionArgs, StoreSession } from '../../../__generated__/schema';
|
|
3
|
-
export declare const validateSession: (_: any, { session: oldSession, search }: MutationValidateSessionArgs, { clients, headers, account }: GraphqlContext) => Promise<StoreSession | null>;
|
|
3
|
+
export declare const validateSession: (_: any, { session: oldSession, search }: MutationValidateSessionArgs, { clients, headers, account, storage, discoveryConfig }: GraphqlContext) => Promise<StoreSession | null>;
|
|
4
4
|
//# sourceMappingURL=validateSession.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validateSession.d.ts","sourceRoot":"","sources":["../../../../../src/platforms/vtex/resolvers/validateSession.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,IAAI,CAAA;AACxC,OAAO,KAAK,EACV,2BAA2B,EAC3B,YAAY,EACb,MAAM,+BAA+B,CAAA;
|
|
1
|
+
{"version":3,"file":"validateSession.d.ts","sourceRoot":"","sources":["../../../../../src/platforms/vtex/resolvers/validateSession.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,IAAI,CAAA;AACxC,OAAO,KAAK,EACV,2BAA2B,EAC3B,YAAY,EACb,MAAM,+BAA+B,CAAA;AAsItC,eAAO,MAAM,eAAe,GAC1B,GAAG,GAAG,EACN,iCAAiC,2BAA2B,EAC5D,yDAAyD,cAAc,KACtE,OAAO,CAAC,YAAY,GAAG,IAAI,CAgJ7B,CAAA"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OrderForm } from '../clients/commerce/types/OrderForm';
|
|
1
2
|
import { Channel } from './channel';
|
|
2
3
|
/**
|
|
3
4
|
* When session SC and orderForm SC diverge, keep Checkout on the orderForm
|
|
@@ -8,4 +9,11 @@ import { Channel } from './channel';
|
|
|
8
9
|
* items that exist only on the orderForm's sales channel.
|
|
9
10
|
*/
|
|
10
11
|
export declare function channelWhenSessionDivergesFromOrderForm(currentChannel: Required<Channel>, orderFormSalesChannel: string | null | undefined): string | null;
|
|
12
|
+
/**
|
|
13
|
+
* Checkout stores an SC on the orderForm only after an items mutation sent
|
|
14
|
+
* with `sc`. An SC-less request on an orderForm that never had items falls
|
|
15
|
+
* back to the platform default (SC 1), so its SC is meaningful only when the
|
|
16
|
+
* orderForm has items or already matches the session.
|
|
17
|
+
*/
|
|
18
|
+
export declare function shouldTrustOrderFormSalesChannel(orderForm: Pick<OrderForm, 'items' | 'salesChannel'>, sessionSalesChannel: string | number, rejectedSalesChannel?: string): boolean;
|
|
11
19
|
//# sourceMappingURL=cartSalesChannel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cartSalesChannel.d.ts","sourceRoot":"","sources":["../../../../../src/platforms/vtex/utils/cartSalesChannel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAGxC;;;;;;;GAOG;AACH,wBAAgB,uCAAuC,CACrD,cAAc,EAAE,QAAQ,CAAC,OAAO,CAAC,EACjC,qBAAqB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/C,MAAM,GAAG,IAAI,CAgBf"}
|
|
1
|
+
{"version":3,"file":"cartSalesChannel.d.ts","sourceRoot":"","sources":["../../../../../src/platforms/vtex/utils/cartSalesChannel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qCAAqC,CAAA;AACpE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAGxC;;;;;;;GAOG;AACH,wBAAgB,uCAAuC,CACrD,cAAc,EAAE,QAAQ,CAAC,OAAO,CAAC,EACjC,qBAAqB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/C,MAAM,GAAG,IAAI,CAgBf;AAED;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAC9C,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,OAAO,GAAG,cAAc,CAAC,EAEpD,mBAAmB,EAAE,MAAM,GAAG,MAAM,EAEpC,oBAAoB,CAAC,EAAE,MAAM,GAC5B,OAAO,CAkBT"}
|
|
@@ -1,10 +1,45 @@
|
|
|
1
1
|
import { Channel } from './channel';
|
|
2
|
+
/**
|
|
3
|
+
* Why the session SC must not be replaced by Session Manager:
|
|
4
|
+
* - `orderForm`: adopted from an orderForm with items (e.g. after Quick Order);
|
|
5
|
+
* kept until the cart is empty.
|
|
6
|
+
* - `url`: derived from the URL by localization; always sent to Checkout.
|
|
7
|
+
*
|
|
8
|
+
* Lives only in the raw `channel` JSON string so the public `Channel` type
|
|
9
|
+
* stays unchanged.
|
|
10
|
+
*/
|
|
11
|
+
export type SalesChannelSource = 'orderForm' | 'url';
|
|
12
|
+
/**
|
|
13
|
+
* Reads the marker from the raw channel string. A `url` marker only means
|
|
14
|
+
* something while localization is enabled; a leftover one (e.g. after a store
|
|
15
|
+
* turns localization off) is ignored so Session Manager decides again.
|
|
16
|
+
*/
|
|
17
|
+
export declare function salesChannelSourceOf(channel: string | null | undefined, { localizationEnabled }?: {
|
|
18
|
+
localizationEnabled?: boolean;
|
|
19
|
+
}): SalesChannelSource | undefined;
|
|
20
|
+
/** Whether the store config enables localization (URL-derived SC). */
|
|
21
|
+
export declare const isLocalizationEnabled: (discoveryConfig: Record<string, unknown> | undefined) => boolean;
|
|
22
|
+
/**
|
|
23
|
+
* SC that Session Manager rejected (401/403) for this shopper after it was
|
|
24
|
+
* adopted from the orderForm. validateCart must not adopt it again, otherwise
|
|
25
|
+
* adoption and rejection would loop.
|
|
26
|
+
*/
|
|
27
|
+
export declare function rejectedSalesChannelOf(channel: string | null | undefined): string | undefined;
|
|
2
28
|
/**
|
|
3
29
|
* Builds the session `channel` string after Session Manager responds.
|
|
4
30
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
31
|
+
* Session Manager is the SC authority, except when the client channel carries
|
|
32
|
+
* a `salesChannelSource` marker: then the client SC is kept and the marker is
|
|
33
|
+
* re-emitted so it survives this validation. A recorded rejection is also
|
|
34
|
+
* re-emitted until Session Manager resolves that SC for the shopper.
|
|
35
|
+
*/
|
|
36
|
+
export declare function channelAfterSessionManager(channel: Required<Channel>, storeChannelValue: string | null | undefined, regionId: string | null | undefined, sellerId: string | undefined, salesChannelSource?: SalesChannelSource, rejectedSalesChannel?: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* SC to request from Session Manager. FastStore 4.6–4.9 could move a session
|
|
39
|
+
* to the SC 1 fallback of an empty orderForm and pin it there. When an
|
|
40
|
+
* unmarked session is on SC 1 but the store runs on another SC, ask for the
|
|
41
|
+
* store SC instead: if Session Manager assigns SC 1 on purpose (e.g. by
|
|
42
|
+
* profile), it still answers SC 1.
|
|
8
43
|
*/
|
|
9
|
-
export declare function
|
|
44
|
+
export declare function salesChannelToRequest(channel: Required<Channel>, salesChannelSource: SalesChannelSource | undefined, storeSalesChannel: string | undefined, search: string): string;
|
|
10
45
|
//# sourceMappingURL=sessionChannel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessionChannel.d.ts","sourceRoot":"","sources":["../../../../../src/platforms/vtex/utils/sessionChannel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAGxC
|
|
1
|
+
{"version":3,"file":"sessionChannel.d.ts","sourceRoot":"","sources":["../../../../../src/platforms/vtex/utils/sessionChannel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAGxC;;;;;;;;GAQG;AACH,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,KAAK,CAAA;AAUpD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAClC,EAAE,mBAA0B,EAAE,GAAE;IAAE,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAAO,GACrE,kBAAkB,GAAG,SAAS,CAQhC;AAED,sEAAsE;AACtE,eAAO,MAAM,qBAAqB,GAChC,iBAAiB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,YAKnD,CAAA;AAEH;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACjC,MAAM,GAAG,SAAS,CAMpB;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,EAC1B,iBAAiB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC5C,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,kBAAkB,CAAC,EAAE,kBAAkB,EACvC,oBAAoB,CAAC,EAAE,MAAM,GAC5B,MAAM,CAkBR;AAKD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,EAC1B,kBAAkB,EAAE,kBAAkB,GAAG,SAAS,EAClD,iBAAiB,EAAE,MAAM,GAAG,SAAS,EACrC,MAAM,EAAE,MAAM,GACb,MAAM,CAUR"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@faststore/api",
|
|
3
|
-
"version": "4.9.3-dev.
|
|
3
|
+
"version": "4.9.3-dev.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"typings": "dist/src/index.d.ts",
|
|
@@ -58,11 +58,11 @@
|
|
|
58
58
|
"vite": "7.1.5",
|
|
59
59
|
"vite-plugin-dts": "4.5.4",
|
|
60
60
|
"vitest": "4.0.18",
|
|
61
|
-
"@faststore/diagnostics": "^4.9.3-dev.
|
|
61
|
+
"@faststore/diagnostics": "^4.9.3-dev.1"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"graphql": "16.11.0",
|
|
65
|
-
"@faststore/diagnostics": "4.9.3-dev.
|
|
65
|
+
"@faststore/diagnostics": "4.9.3-dev.1"
|
|
66
66
|
},
|
|
67
67
|
"publishConfig": {
|
|
68
68
|
"access": "public"
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { logger, OTELAPI } from '@faststore/diagnostics'
|
|
2
2
|
import { name, version } from '../../package.json' with { type: 'json' }
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/** OpenTelemetry log emitter; a no-op while telemetry is disabled. */
|
|
5
|
+
export const OTELLogger = logger('@faststore/api')
|
|
5
6
|
|
|
6
7
|
export const ResolverTrace = <
|
|
7
8
|
TContext extends {
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import deepEquals from 'fast-deep-equal'
|
|
2
2
|
|
|
3
3
|
import { parse } from 'cookie'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
channelWhenSessionDivergesFromOrderForm,
|
|
6
|
+
shouldTrustOrderFormSalesChannel,
|
|
7
|
+
} from '../utils/cartSalesChannel'
|
|
8
|
+
import {
|
|
9
|
+
isLocalizationEnabled,
|
|
10
|
+
rejectedSalesChannelOf,
|
|
11
|
+
salesChannelSourceOf,
|
|
12
|
+
} from '../utils/sessionChannel'
|
|
5
13
|
import { mutateChannelContext, mutateLocaleContext } from '../utils/contex'
|
|
6
14
|
import { md5 } from '../utils/md5'
|
|
7
15
|
import {
|
|
@@ -381,11 +389,59 @@ const getCookieCheckoutOrderNumber = (ctx: string, nameCookie: string) => {
|
|
|
381
389
|
return cookieValue ? cookieValue.split('=')[1] : ''
|
|
382
390
|
}
|
|
383
391
|
|
|
384
|
-
/**
|
|
392
|
+
/**
|
|
393
|
+
* Fetches the orderForm, omitting `sc` for existing carts so Checkout keeps the
|
|
394
|
+
* SC stored on the cart (unless the session SC comes from the URL).
|
|
395
|
+
*
|
|
396
|
+
* Checkout only stores an SC after an items mutation with `sc`. An empty
|
|
397
|
+
* orderForm fetched without `sc` may report the platform default (SC 1):
|
|
398
|
+
* refetch it with the session SC instead of trusting (and adopting) it. Same
|
|
399
|
+
* for an orderForm on an SC Session Manager rejected for this shopper.
|
|
400
|
+
*/
|
|
401
|
+
const getOrderForm = async (
|
|
402
|
+
ctx: GraphqlContext,
|
|
403
|
+
orderFormId: string | undefined,
|
|
404
|
+
isUrlSalesChannel: boolean,
|
|
405
|
+
rejectedSalesChannel: string | undefined
|
|
406
|
+
) => {
|
|
407
|
+
const { commerce } = ctx.clients
|
|
408
|
+
const orderForm = await commerce.checkout.orderForm({
|
|
409
|
+
id: orderFormId,
|
|
410
|
+
channel: ctx.storage.channel,
|
|
411
|
+
preserveSalesChannel: Boolean(orderFormId) && !isUrlSalesChannel,
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
if (
|
|
415
|
+
!orderFormId ||
|
|
416
|
+
shouldTrustOrderFormSalesChannel(
|
|
417
|
+
orderForm,
|
|
418
|
+
ctx.storage.channel.salesChannel,
|
|
419
|
+
rejectedSalesChannel
|
|
420
|
+
)
|
|
421
|
+
) {
|
|
422
|
+
return orderForm
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return commerce.checkout.orderForm({
|
|
426
|
+
id: orderFormId,
|
|
427
|
+
channel: ctx.storage.channel,
|
|
428
|
+
})
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Keep Checkout on the orderForm SC when the browser session lags behind it.
|
|
433
|
+
* Only orderForms with items have a stored SC worth protecting, and a
|
|
434
|
+
* URL-derived (localization) session SC always wins.
|
|
435
|
+
*/
|
|
385
436
|
const adoptOrderFormSalesChannelWhenSessionDiverges = (
|
|
386
437
|
ctx: GraphqlContext,
|
|
387
|
-
orderForm: OrderForm
|
|
438
|
+
orderForm: OrderForm,
|
|
439
|
+
canAdopt: boolean
|
|
388
440
|
): string | null => {
|
|
441
|
+
if (!canAdopt || orderForm.items.length === 0) {
|
|
442
|
+
return null
|
|
443
|
+
}
|
|
444
|
+
|
|
389
445
|
const adoptedChannel = channelWhenSessionDivergesFromOrderForm(
|
|
390
446
|
ctx.storage.channel,
|
|
391
447
|
orderForm.salesChannel
|
|
@@ -442,6 +498,12 @@ export const validateCart = async (
|
|
|
442
498
|
|
|
443
499
|
const channel = session?.channel
|
|
444
500
|
const locale = session?.locale
|
|
501
|
+
// Localization derives the SC from the URL: never omit `sc` nor adopt.
|
|
502
|
+
const isUrlSalesChannel =
|
|
503
|
+
salesChannelSourceOf(channel, {
|
|
504
|
+
localizationEnabled: isLocalizationEnabled(ctx.discoveryConfig),
|
|
505
|
+
}) === 'url'
|
|
506
|
+
const rejectedSalesChannel = rejectedSalesChannelOf(channel)
|
|
445
507
|
|
|
446
508
|
if (channel) {
|
|
447
509
|
mutateChannelContext(ctx, channel)
|
|
@@ -457,12 +519,12 @@ export const validateCart = async (
|
|
|
457
519
|
// session SC (e.g. after Quick Order) would recalculate the cart and drop
|
|
458
520
|
// items only available in the orderForm's trade policy. New carts still
|
|
459
521
|
// send `sc` from the session (see commerce.checkout.orderForm).
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
522
|
+
const orderForm = await getOrderForm(
|
|
523
|
+
ctx,
|
|
524
|
+
orderFormIdFromCookie || undefined,
|
|
525
|
+
isUrlSalesChannel,
|
|
526
|
+
rejectedSalesChannel
|
|
527
|
+
)
|
|
466
528
|
const orderNumber = orderForm.orderFormId
|
|
467
529
|
|
|
468
530
|
// Clear messages so it doesn't keep populating toasts on a loop
|
|
@@ -486,7 +548,8 @@ export const validateCart = async (
|
|
|
486
548
|
// on the trade policy that actually owns the items.
|
|
487
549
|
const adoptedSalesChannel = adoptOrderFormSalesChannelWhenSessionDiverges(
|
|
488
550
|
ctx,
|
|
489
|
-
orderForm
|
|
551
|
+
orderForm,
|
|
552
|
+
!isUrlSalesChannel
|
|
490
553
|
)
|
|
491
554
|
|
|
492
555
|
const newOrderForm = await setOrderFormEtag(
|
|
@@ -509,7 +572,8 @@ export const validateCart = async (
|
|
|
509
572
|
// items that exist only on the orderForm's sales channel.
|
|
510
573
|
const adoptedSalesChannel = adoptOrderFormSalesChannelWhenSessionDiverges(
|
|
511
574
|
ctx,
|
|
512
|
-
orderForm
|
|
575
|
+
orderForm,
|
|
576
|
+
!isUrlSalesChannel
|
|
513
577
|
)
|
|
514
578
|
|
|
515
579
|
// Step2: Process items from both browser and checkout so they have the same shape
|
|
@@ -6,7 +6,16 @@ import type {
|
|
|
6
6
|
StoreSession,
|
|
7
7
|
} from '../../../__generated__/schema'
|
|
8
8
|
import ChannelMarshal from '../utils/channel'
|
|
9
|
-
import {
|
|
9
|
+
import { OTELLogger } from '../../../observability/telemetry'
|
|
10
|
+
import { FastStoreError } from '../../errors'
|
|
11
|
+
import {
|
|
12
|
+
channelAfterSessionManager,
|
|
13
|
+
isLocalizationEnabled,
|
|
14
|
+
rejectedSalesChannelOf,
|
|
15
|
+
type SalesChannelSource,
|
|
16
|
+
salesChannelSourceOf,
|
|
17
|
+
salesChannelToRequest,
|
|
18
|
+
} from '../utils/sessionChannel'
|
|
10
19
|
import {
|
|
11
20
|
buildB2bSession,
|
|
12
21
|
buildMarketingData,
|
|
@@ -28,12 +37,127 @@ type SessionCheckoutNamespace = {
|
|
|
28
37
|
regionId?: { value?: string | null } | null
|
|
29
38
|
}
|
|
30
39
|
|
|
40
|
+
const isSalesChannelRejected = (error: unknown) =>
|
|
41
|
+
error instanceof FastStoreError &&
|
|
42
|
+
(error.extensions.status === 401 || error.extensions.status === 403)
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Whether the rejection is about the requested SC (e.g. "You must be logged
|
|
46
|
+
* in to access the requested SalesChannel"), not an unrelated auth failure.
|
|
47
|
+
* Only then is an adopted SC recorded as rejected.
|
|
48
|
+
*/
|
|
49
|
+
const isAboutSalesChannel = (error: unknown) =>
|
|
50
|
+
error instanceof Error && /sales\s*channel/i.test(error.message)
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Whether a Session Manager rejection drops the `orderForm` adoption: only
|
|
54
|
+
* when the rejected `sc` is the adopted SC itself and the error is about the
|
|
55
|
+
* sales channel. A rejected `?sc=` from the page URL or an unrelated 401/403
|
|
56
|
+
* keeps the adoption.
|
|
57
|
+
*/
|
|
58
|
+
const rejectsAdoption = (
|
|
59
|
+
error: unknown,
|
|
60
|
+
salesChannelSource: SalesChannelSource | undefined,
|
|
61
|
+
requestedSalesChannel: string | undefined,
|
|
62
|
+
clientSalesChannel: string
|
|
63
|
+
) => {
|
|
64
|
+
if (
|
|
65
|
+
salesChannelSource !== 'orderForm' ||
|
|
66
|
+
requestedSalesChannel !== clientSalesChannel
|
|
67
|
+
) {
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (isAboutSalesChannel(error)) {
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Session Manager has no structured code for this; make a wording change
|
|
76
|
+
// observable (stdout and OpenTelemetry logs) instead of silently keeping
|
|
77
|
+
// the adoption.
|
|
78
|
+
const message = `[validateSession] Session Manager rejected adopted sales channel ${requestedSalesChannel} with an unrecognized error; keeping the adoption.`
|
|
79
|
+
const detail = error instanceof Error ? error.message : error
|
|
80
|
+
|
|
81
|
+
console.warn(message, detail)
|
|
82
|
+
OTELLogger('warn', '%s %o', message, detail)
|
|
83
|
+
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Calls Session Manager. When it rejects the requested `sc` (401/403), retries
|
|
89
|
+
* once without it so Session Manager resolves an SC the shopper can use:
|
|
90
|
+
* - no marker: a stale SC (e.g. left by an older version) would stick forever;
|
|
91
|
+
* - `orderForm` marker: if the adopted SC itself is not available to this
|
|
92
|
+
* shopper, the adoption is dropped and the SC is recorded as rejected
|
|
93
|
+
* (see `rejectsAdoption`);
|
|
94
|
+
* - `url` marker: the URL SC is intentional, so no retry.
|
|
95
|
+
* If the retry also fails, the current channel is kept as is.
|
|
96
|
+
*/
|
|
97
|
+
const fetchSessionData = async (
|
|
98
|
+
clients: GraphqlContext['clients'],
|
|
99
|
+
params: URLSearchParams,
|
|
100
|
+
salesChannelSource: SalesChannelSource | undefined,
|
|
101
|
+
clientSalesChannel: string
|
|
102
|
+
) => {
|
|
103
|
+
const unchanged = { salesChannelSource, rejectedSalesChannel: undefined }
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
return {
|
|
107
|
+
...unchanged,
|
|
108
|
+
sessionData: await clients.commerce.session(params.toString()),
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (salesChannelSource === 'url' || !isSalesChannelRejected(error)) {
|
|
112
|
+
return { ...unchanged, sessionData: null }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const requestedSalesChannel = params.get('sc') ?? undefined
|
|
116
|
+
const dropsAdoption = rejectsAdoption(
|
|
117
|
+
error,
|
|
118
|
+
salesChannelSource,
|
|
119
|
+
requestedSalesChannel,
|
|
120
|
+
clientSalesChannel
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
params.delete('sc')
|
|
124
|
+
|
|
125
|
+
const sessionData = await clients.commerce
|
|
126
|
+
.session(params.toString())
|
|
127
|
+
.catch(() => null)
|
|
128
|
+
|
|
129
|
+
if (!sessionData || !dropsAdoption) {
|
|
130
|
+
return { ...unchanged, sessionData }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
sessionData,
|
|
135
|
+
salesChannelSource: undefined,
|
|
136
|
+
rejectedSalesChannel: requestedSalesChannel,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
31
141
|
export const validateSession = async (
|
|
32
142
|
_: any,
|
|
33
143
|
{ session: oldSession, search }: MutationValidateSessionArgs,
|
|
34
|
-
{ clients, headers, account }: GraphqlContext
|
|
144
|
+
{ clients, headers, account, storage, discoveryConfig }: GraphqlContext
|
|
35
145
|
): Promise<StoreSession | null> => {
|
|
36
|
-
const
|
|
146
|
+
const clientChannel = ChannelMarshal.parse(oldSession.channel ?? '')
|
|
147
|
+
const incomingSalesChannelSource = salesChannelSourceOf(oldSession.channel, {
|
|
148
|
+
localizationEnabled: isLocalizationEnabled(discoveryConfig),
|
|
149
|
+
})
|
|
150
|
+
const channel = {
|
|
151
|
+
...clientChannel,
|
|
152
|
+
salesChannel: salesChannelToRequest(
|
|
153
|
+
clientChannel,
|
|
154
|
+
incomingSalesChannelSource,
|
|
155
|
+
storage?.channel?.salesChannel
|
|
156
|
+
? String(storage.channel.salesChannel)
|
|
157
|
+
: undefined,
|
|
158
|
+
search
|
|
159
|
+
),
|
|
160
|
+
}
|
|
37
161
|
const postalCode = String(oldSession.postalCode ?? '')
|
|
38
162
|
const country = oldSession.country ?? ''
|
|
39
163
|
let city = oldSession.city ?? null
|
|
@@ -69,9 +193,13 @@ export const validateSession = async (
|
|
|
69
193
|
account
|
|
70
194
|
)
|
|
71
195
|
|
|
72
|
-
const sessionData
|
|
73
|
-
|
|
74
|
-
|
|
196
|
+
const { sessionData, salesChannelSource, rejectedSalesChannel } =
|
|
197
|
+
await fetchSessionData(
|
|
198
|
+
clients,
|
|
199
|
+
params,
|
|
200
|
+
incomingSalesChannelSource,
|
|
201
|
+
String(channel.salesChannel ?? '')
|
|
202
|
+
)
|
|
75
203
|
|
|
76
204
|
const profile = sessionData?.namespaces.profile ?? null
|
|
77
205
|
const shopper = sessionData?.namespaces.shopper ?? null
|
|
@@ -101,9 +229,17 @@ export const validateSession = async (
|
|
|
101
229
|
postalCode,
|
|
102
230
|
geoCoordinates,
|
|
103
231
|
country,
|
|
104
|
-
params.get('sc') ?? channel.salesChannel
|
|
232
|
+
params.get('sc') ?? store?.channel?.value ?? channel.salesChannel
|
|
105
233
|
)
|
|
106
234
|
|
|
235
|
+
const person = buildPersonFromProfile(profile)
|
|
236
|
+
// A rejection belongs to the shopper it was recorded for: forget it when
|
|
237
|
+
// they log in or out, so a new identity can have the SC adopted again.
|
|
238
|
+
const previousRejection =
|
|
239
|
+
(oldSession.person?.id ?? null) === (person?.id ?? null)
|
|
240
|
+
? rejectedSalesChannelOf(oldSession.channel)
|
|
241
|
+
: undefined
|
|
242
|
+
|
|
107
243
|
const newSession = {
|
|
108
244
|
...oldSession,
|
|
109
245
|
currency: {
|
|
@@ -115,7 +251,9 @@ export const validateSession = async (
|
|
|
115
251
|
channel,
|
|
116
252
|
store?.channel?.value,
|
|
117
253
|
checkout?.regionId?.value,
|
|
118
|
-
sellerId
|
|
254
|
+
sellerId,
|
|
255
|
+
salesChannelSource,
|
|
256
|
+
rejectedSalesChannel ?? previousRejection
|
|
119
257
|
),
|
|
120
258
|
/**
|
|
121
259
|
* B2B data structure in Session:
|
|
@@ -134,7 +272,7 @@ export const validateSession = async (
|
|
|
134
272
|
unitId,
|
|
135
273
|
}),
|
|
136
274
|
marketingData,
|
|
137
|
-
person
|
|
275
|
+
person,
|
|
138
276
|
geoCoordinates:
|
|
139
277
|
(geoCoordinates?.latitude &&
|
|
140
278
|
geoCoordinates?.longitude &&
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { OrderForm } from '../clients/commerce/types/OrderForm'
|
|
1
2
|
import type { Channel } from './channel'
|
|
2
3
|
import ChannelMarshal from './channel'
|
|
3
4
|
|
|
@@ -29,3 +30,35 @@ export function channelWhenSessionDivergesFromOrderForm(
|
|
|
29
30
|
hasOnlyDefaultSalesChannel: false,
|
|
30
31
|
})
|
|
31
32
|
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Checkout stores an SC on the orderForm only after an items mutation sent
|
|
36
|
+
* with `sc`. An SC-less request on an orderForm that never had items falls
|
|
37
|
+
* back to the platform default (SC 1), so its SC is meaningful only when the
|
|
38
|
+
* orderForm has items or already matches the session.
|
|
39
|
+
*/
|
|
40
|
+
export function shouldTrustOrderFormSalesChannel(
|
|
41
|
+
orderForm: Pick<OrderForm, 'items' | 'salesChannel'>,
|
|
42
|
+
// Persisted sessions may carry a numeric SC (e.g. `{"salesChannel":2}`).
|
|
43
|
+
sessionSalesChannel: string | number,
|
|
44
|
+
// SC that Session Manager rejected for this shopper (see validateSession).
|
|
45
|
+
rejectedSalesChannel?: string
|
|
46
|
+
): boolean {
|
|
47
|
+
if (
|
|
48
|
+
rejectedSalesChannel != null &&
|
|
49
|
+
String(orderForm.salesChannel ?? '') === rejectedSalesChannel &&
|
|
50
|
+
String(sessionSalesChannel ?? '') !== rejectedSalesChannel
|
|
51
|
+
) {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (orderForm.items.length > 0) {
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (orderForm.salesChannel == null || orderForm.salesChannel === '') {
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return String(orderForm.salesChannel) === String(sessionSalesChannel ?? '')
|
|
64
|
+
}
|