Skip to content

Commit 764b4f9

Browse files
authored
ProductDoctor: Group channel issues by purchasability vs shipping (#6547)
1 parent c305dd4 commit 764b4f9

12 files changed

Lines changed: 428 additions & 58 deletions

.changeset/silver-foxes-jump.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"saleor-dashboard": patch
3+
---
4+
5+
ProductDoctor: Group channel issues by purchasability vs shipping to mirror Saleor 3.23 stock-availability semantics

.github/workflows/deploy-cloud.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ jobs:
5252
ENABLED_SERVICE_NAME_HEADER: true
5353
ONBOARDING_USER_JOINED_DATE_THRESHOLD: ${{ vars.CLOUD_ONBOARDING_USER_JOINED_DATE_THRESHOLD }}
5454
steps:
55-
5655
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
5756
with:
5857
ref: refs/tags/${{ github.event.client_payload.ref }}
@@ -94,7 +93,7 @@ jobs:
9493
deploy:
9594
runs-on: ubuntu-24.04
9695
needs: [build]
97-
96+
9897
environment:
9998
deployment: false
10099
name: deploy-cloud

locale/defaultMessages.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4196,6 +4196,10 @@
41964196
"context": "Charged transaction amount, data display header",
41974197
"string": "Charged"
41984198
},
4199+
"LYKO9m": {
4200+
"context": "Section heading for issues that affect order fulfillment (shipping zones, shipping methods)",
4201+
"string": "Shipping"
4202+
},
41994203
"La3xKC": {
42004204
"context": "Description of public API verification feature",
42014205
"string": "Check if customers can actually purchase this product"
@@ -6180,6 +6184,10 @@
61806184
"WY3IXU": {
61816185
"string": "This code already exists"
61826186
},
6187+
"WZ2kxO": {
6188+
"context": "Section heading for issues that affect whether a customer can purchase (warehouses, stock, channel listing, pricing)",
6189+
"string": "Purchasability"
6190+
},
61836191
"WasHjQ": {
61846192
"context": "voucher discount",
61856193
"string": "Shipment"
@@ -7709,10 +7717,6 @@
77097717
"context": "order transaction refund summary description",
77107718
"string": "The refund amount is calculated automatically based on the items selected. You can modify it manually if needed."
77117719
},
7712-
"f71A+Y": {
7713-
"context": "Title for delivery configuration section in channel",
7714-
"string": "Delivery configuration"
7715-
},
77167720
"fDc5ys": {
77177721
"context": "order transaction refund summary label",
77187722
"string": "Selected products"

src/products/components/ProductDoctor/AvailabilityCard.test.tsx

Lines changed: 176 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ describe("AvailabilityCard / DiagnosticSummaryBanner", () => {
7474
useLegacyShippingZoneStockAvailability: false,
7575
issues: [
7676
makeIssue({
77+
category: "purchasability",
7778
id: "stock-outside-channel-warehouses",
7879
severity: "info",
7980
message: "Stranded stock",
@@ -102,11 +103,13 @@ describe("AvailabilityCard / DiagnosticSummaryBanner", () => {
102103
const diagnostics = baseDiagnostics({
103104
issues: [
104105
makeIssue({
106+
category: "purchasability",
105107
id: "stock-outside-channel-warehouses",
106108
severity: "info",
107109
message: "Stranded stock — warehouse A",
108110
}),
109111
makeIssue({
112+
category: "purchasability",
110113
id: "stock-outside-channel-warehouses",
111114
severity: "info",
112115
message: "Stranded stock — warehouse B",
@@ -140,8 +143,14 @@ describe("AvailabilityCard / DiagnosticSummaryBanner", () => {
140143
const diagnostics = baseDiagnostics({
141144
useLegacyShippingZoneStockAvailability: true,
142145
issues: [
143-
makeIssue({ id: "no-stock", severity: "warning", message: "No stock" }),
144146
makeIssue({
147+
category: "purchasability",
148+
id: "no-stock",
149+
severity: "warning",
150+
message: "No stock",
151+
}),
152+
makeIssue({
153+
category: "purchasability",
145154
id: "stock-outside-channel-warehouses",
146155
severity: "info",
147156
message: "Stranded stock",
@@ -207,12 +216,21 @@ describe("AvailabilityCard / StockAvailabilityModeIndicator", () => {
207216
});
208217
});
209218

210-
// Synthetic issue factory for UI tests. Defaults to a genuine info-level
211-
// advisory (stranded stock — the only check that emits info severity) so
212-
// callers using `makeIssue({ severity: "info" })` get an unambiguously
213-
// realistic fixture. Tests that need warnings/errors override the id +
214-
// severity explicitly.
215-
const makeIssue = (overrides: Partial<AvailabilityIssue> = {}): AvailabilityIssue => ({
219+
/**
220+
* Synthetic issue factory for UI tests. `category` must be passed explicitly
221+
* — we deliberately avoid inferring it from `id` in the test layer because
222+
* that would duplicate the production category mapping (which lives in
223+
* `runAvailabilityChecks`) and silently miscategorize new issue types.
224+
*
225+
* Defaults to a genuine info-level advisory (stranded stock — the only check
226+
* that emits `info` severity in production) so callers using
227+
* `makeIssue({ category: "purchasability", severity: "info" })` get an
228+
* unambiguously realistic fixture. Tests that need warnings/errors override
229+
* the id + severity + message explicitly.
230+
*/
231+
const makeIssue = (
232+
overrides: Partial<AvailabilityIssue> & Pick<AvailabilityIssue, "category">,
233+
): AvailabilityIssue => ({
216234
id: "stock-outside-channel-warehouses",
217235
severity: "info",
218236
channelId: "channel-1",
@@ -236,7 +254,10 @@ describe("AvailabilityCard channel header severity gating", () => {
236254
// Arrange — direct mode, single info advisory.
237255
const diagnostics = baseDiagnostics({
238256
useLegacyShippingZoneStockAvailability: false,
239-
issues: [makeIssue({ severity: "info" })],
257+
// Default fixture id (`stock-outside-channel-warehouses`) is a
258+
// purchasability-category check; pair the override accordingly so the
259+
// fixture stays internally consistent with production grouping.
260+
issues: [makeIssue({ category: "purchasability", severity: "info" })],
240261
hasErrors: false,
241262
hasWarnings: false,
242263
});
@@ -262,7 +283,14 @@ describe("AvailabilityCard channel header severity gating", () => {
262283
// Arrange — legacy mode, one warning-level issue.
263284
const diagnostics = baseDiagnostics({
264285
useLegacyShippingZoneStockAvailability: true,
265-
issues: [makeIssue({ id: "no-stock", severity: "warning", message: "No stock" })],
286+
issues: [
287+
makeIssue({
288+
category: "purchasability",
289+
id: "no-stock",
290+
severity: "warning",
291+
message: "No stock",
292+
}),
293+
],
266294
hasErrors: false,
267295
hasWarnings: true,
268296
});
@@ -286,17 +314,32 @@ describe("AvailabilityCard channel header severity gating", () => {
286314

287315
it("renders a visible count and the warning icon when multiple header-worthy issues exist alongside info advisories", () => {
288316
// Arrange — two warnings + two info advisories on the same channel.
317+
// Both info advisories are stranded-stock fixtures since that's the only
318+
// check that genuinely emits info severity after Option A (no-shipping-
319+
// zones is now a warning in both modes).
289320
const diagnostics = baseDiagnostics({
290321
useLegacyShippingZoneStockAvailability: false,
291322
issues: [
292-
makeIssue({ id: "no-stock", severity: "warning", message: "No stock" }),
293-
makeIssue({ id: "no-warehouses", severity: "warning", message: "No warehouses" }),
294323
makeIssue({
324+
category: "purchasability",
325+
id: "no-stock",
326+
severity: "warning",
327+
message: "No stock",
328+
}),
329+
makeIssue({
330+
category: "purchasability",
331+
id: "no-warehouses",
332+
severity: "warning",
333+
message: "No warehouses",
334+
}),
335+
makeIssue({
336+
category: "purchasability",
295337
id: "stock-outside-channel-warehouses",
296338
severity: "info",
297339
message: "Stranded stock A",
298340
}),
299341
makeIssue({
342+
category: "purchasability",
300343
id: "stock-outside-channel-warehouses",
301344
severity: "info",
302345
message: "Stranded stock B",
@@ -325,9 +368,20 @@ describe("AvailabilityCard channel header severity gating", () => {
325368
const diagnostics = baseDiagnostics({
326369
useLegacyShippingZoneStockAvailability: true,
327370
issues: [
328-
makeIssue({ id: "no-variants", severity: "error", message: "No variants" }),
329-
makeIssue({ id: "no-stock", severity: "warning", message: "No stock" }),
330371
makeIssue({
372+
category: "purchasability",
373+
id: "no-variants",
374+
severity: "error",
375+
message: "No variants",
376+
}),
377+
makeIssue({
378+
category: "purchasability",
379+
id: "no-stock",
380+
severity: "warning",
381+
message: "No stock",
382+
}),
383+
makeIssue({
384+
category: "purchasability",
331385
id: "stock-outside-channel-warehouses",
332386
severity: "info",
333387
message: "Info only",
@@ -611,3 +665,112 @@ describe("PublicApiVerificationBadge reassurance", () => {
611665
expect(screen.queryByTestId("verification-reassurance")).toBeNull();
612666
});
613667
});
668+
669+
describe("AvailabilityChannelItem issue category sections", () => {
670+
const renderExpandedChannel = (issues: AvailabilityIssue[]) => {
671+
const diagnostics = baseDiagnostics({
672+
issues,
673+
hasErrors: issues.some(i => i.severity === "error"),
674+
hasWarnings: issues.some(i => i.severity === "warning"),
675+
});
676+
677+
render(<AvailabilityCard diagnostics={diagnostics} totalChannelsCount={1} />, {
678+
wrapper: Wrapper,
679+
});
680+
681+
fireEvent.click(screen.getByText("Default Channel"));
682+
};
683+
684+
it("renders both category sections when issues span both purchasability and shipping", () => {
685+
renderExpandedChannel([
686+
makeIssue({
687+
category: "purchasability",
688+
id: "no-stock",
689+
severity: "warning",
690+
message: "No stock",
691+
}),
692+
makeIssue({
693+
category: "shipping",
694+
id: "no-shipping-zones",
695+
severity: "info",
696+
message: "No shipping zones",
697+
}),
698+
]);
699+
700+
const sections = screen.getAllByTestId("issue-category-section");
701+
702+
expect(sections).toHaveLength(2);
703+
expect(sections.map(s => s.getAttribute("data-test-category"))).toEqual([
704+
"purchasability",
705+
"shipping",
706+
]);
707+
});
708+
709+
it("renders only the purchasability section when no shipping issues exist", () => {
710+
renderExpandedChannel([
711+
makeIssue({
712+
category: "purchasability",
713+
id: "no-variants",
714+
severity: "error",
715+
message: "No variants",
716+
}),
717+
makeIssue({
718+
category: "purchasability",
719+
id: "no-stock",
720+
severity: "warning",
721+
message: "No stock",
722+
}),
723+
]);
724+
725+
const sections = screen.getAllByTestId("issue-category-section");
726+
727+
expect(sections).toHaveLength(1);
728+
expect(sections[0]).toHaveAttribute("data-test-category", "purchasability");
729+
});
730+
731+
it("renders only the shipping section when no purchasability issues exist", () => {
732+
renderExpandedChannel([
733+
makeIssue({
734+
category: "shipping",
735+
id: "no-shipping-zones",
736+
severity: "info",
737+
message: "No shipping zones",
738+
}),
739+
]);
740+
741+
const sections = screen.getAllByTestId("issue-category-section");
742+
743+
expect(sections).toHaveLength(1);
744+
expect(sections[0]).toHaveAttribute("data-test-category", "shipping");
745+
});
746+
747+
it("groups multiple issues of the same category under one section", () => {
748+
renderExpandedChannel([
749+
makeIssue({
750+
category: "purchasability",
751+
id: "no-stock",
752+
severity: "warning",
753+
message: "No stock",
754+
}),
755+
makeIssue({
756+
category: "purchasability",
757+
id: "stock-outside-channel-warehouses",
758+
severity: "info",
759+
message: "Stranded stock",
760+
}),
761+
makeIssue({
762+
category: "purchasability",
763+
id: "no-warehouses",
764+
severity: "warning",
765+
message: "No warehouses",
766+
}),
767+
]);
768+
769+
const sections = screen.getAllByTestId("issue-category-section");
770+
771+
expect(sections).toHaveLength(1);
772+
expect(sections[0]).toHaveAttribute("data-test-category", "purchasability");
773+
// All three callouts should be inside the single purchasability section.
774+
expect(screen.getAllByTestId("availability-issue-callout")).toHaveLength(3);
775+
});
776+
});

0 commit comments

Comments
 (0)